RESTful Node.js: A Structured Approach
  • Book Cover
  • About the Author
  • Links and Resources
  • Part I: The Why
    • Foreword
    • Preface
    • Chapter 1: Introduction
      • The Rise of REST and Distributed Systems
      • Problem #1: Structureless Design, Structureless REST
      • The Emergence of JavaScript and Node.js
      • Problem #2: Structureless JavaScript, Structureless Node.js
      • Behold, the Solution: A Structured Approach
      • Summary
  • Part 2: The Theory
    • Chapter 2: REST Origins
      • A Brief History of the Web and the Birth of REST
      • REST vs. HTTP
      • REST - The Abstract Web Architecture
      • HTTP - A Peak at REST's Concrete Implementation
      • What does it mean for an API to be RESTful?
      • Measuring "RESTfulness" with Richardson Maturity Model
      • Pragmatic REST vs Dogmatic REST
      • Summary
    • Chapter 3: RESTful API Design Guidelines and "Best Practices"
      • Theories vs. Principles vs. Guidelines
      • URI Design
      • Method Verbs
      • Status Codes
      • Representational Design
      • Metadata Design
      • Versioning Strategies
      • Security Considerations
      • Documentation
      • Case Study: GitHub
      • Summary
    • Chapter 4: Structured JavaScript Architecture
      • The Monstrous Monolith and Its Downfall
      • Layered/N-Tier Architecture: The Unpopular Proven Way
      • Microservices and Distributed Computing: A Popular Misdirection
      • Summary
    • Chapter 5: The 8 Step Recipe
      • Route Name (URI)
      • Input Request
      • Middleware
      • Validation
      • Domain
      • Events
      • Output Response
      • Test, Refactor, Document
      • Summary
  • Part 3: The Code
    • Chapter 6: Introduction to the Bookstore API
      • The Bookstore API Endpoint Specifications
      • API Design and Code Structure
      • Project Setup
      • Summary
    • Chapter 7: Retrieving Books from our API
      • Retrieving All Books - Planning
      • Retrieving All Books - Implementation
      • Retrieving A Book By ID - Planning
      • Retrieving A Book By ID - Implementation
      • Summary
    • Chapter 8: Adding Authentication to our API
      • Registering the User - Planning
      • Registering the User - Implementation
      • Logging the User In - Planning
      • Logging the User In - Implementation
      • Getting Authenticated User - Planning
      • Getting Authenticated User - Implementation
      • Summary
    • Chapter 9: Adding the Create, Update, and Delete Operations to our API
      • Creating A Book Listing - Planning
      • Creating A Book Listing - Implementation
      • Updating A Book Listing By ID - Planning
      • Updating A Book Listing By ID - Implementation
      • Deleting A Book Listing By ID - Planning
      • Deleting A Book Listing By ID - Implementation
      • Summary
    • Chapter 10: Testing our API
      • Testing the Request
      • Testing the Middleware
      • Testing the Validation
      • Testing the Domain
      • Testing the Event
      • Testing the Response
      • Testing the Controller
      • Integration Test
      • Summary
  • Conclusion
    • Final Words
  • Bonus!
    • Refactoring to HATEOAS
  • Appendix
    • Sources & References
Powered by GitBook
On this page
  • 1 - Route Name
  • 2 - Input Request
  • 3 - Middleware
  • 4 - Validation
  • 5 - Domain
  • 6 - Events
  • 7 - Response
  • Putting It All Together
Edit on GitHub
  1. Part 3: The Code
  2. Chapter 8: Adding Authentication to our API

Registering the User - Implementation

1 - Route Name

GET /api/v1/users

Let's add an additional users route to the routes/index.js file.

File: src/routes/index.js

const express = require('express')
const router = express.Router()

const userRoutes = require('./user.route')
const bookRoutes = require('./book.route')

function getRouter() {
  router.use('/books', bookRoutes)
  router.use('/users', userRoutes) // our new route

  return router
}

module.exports = getRouter

Here, we will then add a registerUser controller function.

File: src/routes/user.route.js

const express = require('express')
const router = express.Router()

const isAuthenticated = require('../middleware/auth.middleware')
const { registerUser } = require('../controllers/auth')

router.post('/', registerUser)

module.exports = router

We will then proceed to fill up the rest of the controller below.

File: src/controllers/auth.controller.js

/**
 * Inserts the user into the database and fires off an email notification
 * to that user's email if successful.
 */
const registerUser = catchExceptions(async (req, res) => {
  // Our code goes here...
})

2 - Input Request

Now we will add in a custom DTO that will help us specify the contract between us and the client.

This layer can be seem as a validation layer, but what we are ultimately trying to accomplish here is to get the frontend and backend aligned on the same page. Notice there is no real validation of having the password being a certain length or the phone_number being a certain format. The DTO simply helps the client who's calling our API to provide the correct structure in the payload.

File: src/requests/registerUserDTO.js

const ApiException = require('../utils/ApiException')

const fields = [
  'first_name',
  'last_name',
  'email',
  'password',
  'password_confirmation',
  'phone_number'
]

/**
 * @param Object data
 */
const registerUserRequestDto = (data) => {
  const errors = []
  fields.forEach((field) => {
    if (!(field in data)) {
      errors.push(`This DTO's property is required: ${field}.`)
    }
  })

  if (errors.length > 0) {
    throw new ApiException({
      status: 'error',
      code: 422,
      message: 'Input fields are not in the correct form.',
      data: null,
      errors
    })
  }

  return data
}

module.exports = registerUserRequestDto

3 - Middleware

As stated in the previous section, we will be adding in a global middleware to help us catch errors.

You may have noticed a utility wrapper being used in the controllers up until now called catchExceptions.

File: src/controllers/auth.controller.js

const registerUser = catchExceptions(async (req, res) => {
  // Our code goes here...
})

File: src/utils/catchExceptions.js

const catchExceptions = (func) => {
  return (req, res, next) => {
    Promise.resolve(func(req, res, next)).catch(next)
  }
}

module.exports = catchExceptions

The reason for this is due to our globalErrorHandler that we'll be putting as follows.

File: src/server.js

const globalErrorHandler = require('./utils/globalErrorHandler')

app.use(globalErrorHandler)

File: src/utils/globalErrorHandler.js

const globalResponseDTO = require('../responses/globalResponseDTO')

const globalErrorHandler = async (err, req, res, next) => {
  console.log('===============================')
  console.log('Global Error Catcher:', err.name)
  console.log('===============================')

  if (err.name === 'ApiException') {
    console.error('ApiException', err)

    res.status(err.code).json(
      globalResponseDTO({
        status: err.status,
        code: err.code,
        message: err.message,
        data: err.data,
        errors: err.errors
      })
    )
  } else {
    console.error('Other Error', err)
  }
}

module.exports = globalErrorHandler

This is really going to make our lives easier, because every time we throw an ApiException anywhere in our application, it is going to automatically catch the error we have thrown and output it out as JSON output.

4 - Validation

File: src/validators/registerUserValidator.js

const Validator = require('validatorjs')
const ApiException = require('../utils/ApiException')

/**
 * @param {*} data {
 *  - first_name
 *  - last_name
 *  - email
 *  - password
 *  - password_confirm
 *  - phone_number
 * }
 *
 * @returns Validator
 */
const registerUserValidator = (data) => {
  const rules = {
    first_name: 'required',
    last_name: 'required',
    email: 'required|email',
    password: 'required|min:6',
    password_confirmation: 'required|min:6|same:password',
    phone_number: 'required|telephone'
  }

  const validator = new Validator(data, rules)

  if (validator.fails()) {
    let errors = []
    for (const field in validator.errors.errors) {
      errors = errors.concat(validator.errors.errors[field])
    }

    throw new ApiException({
      status: 'error',
      code: 422,
      message: 'There were errors with the validation.',
      data: null,
      errors
    })
  }

  return validator
}

// This is our custom 'telephone' validation rule
Validator.register(
  'telephone',
  function (value) {
    return value.match(/^\(?([0-9]{3})\)?[-.●]?([0-9]{3})[-.●]?([0-9]{4})$/)
  },
  'The :attribute field is not in a correct format.'
)

module.exports = registerUserValidator

5 - Domain

For our domain layer, we'll first create our user model as follows.

File: src/domain/models/user.model.js

const mongoose = require('mongoose')
const Schema = mongoose.Schema

const userModel = new Schema({
  first_name: {
    type: String,
    required: true
  },
  last_name: {
    type: String,
    required: false
  },
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true
  },
  phone_number: {
    type: String,
    required: true
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
})

module.exports = mongoose.model('user', userModel)

Then, we will create the createUser method, which will simply save the userData into our database.

File: src/domain/services/userRepository.js

const UserModel = require('../models/user.model')

/**
 *
 * @param {*} user {
 *  - name
 *  - email
 *  - password
 * }
 *
 * @returns user
 */
const createUser = async (userData) => {
  const user = new UserModel(userData)
  const userReturn = await user.save(userData)

  return userReturn
}

module.exports = {
  createUser
}

Lastly, we will create the authService layer and use our userRepository.createUser() utility to create our service for registering our user.

File: src/domain/services/authService.js

/**
 * @returns User
 */
const registerUser = async (user) => {
  try {
    const createdUser = await userRepository.createUser(user)
    return createdUser
  } catch (err) {
    if (err.code === 11000 && err?.keyPattern?.email === 1) {
      throw new ApiException({
        message: `This email is already taken.`,
        status: 'error',
        code: 409,
        data: null,
        errors: [`This email is already taken.`]
      })
    }
  }
}

Notice how we do a try-catch here where we throw a custom ApiException in order to catch a specific type of error, in this case it's that the email must be unique.

6 - Events

As noted in the planning section of this endpoint, when the user has successfully registered, an event will fire off. We can take advantage of Node.js' built-in eventEmitter to create a pub-sub structure for us.

File: src/events/index.js

const events = require('events')

const eventEmitter = new events.EventEmitter()

const userHasRegistered = require('./userHasRegisteredEvent')

eventEmitter.on('userHasRegistered', userHasRegistered)

File: src/events/userHasRegisteredEvent.js

const mailer = require('../utils/mailer')

/**
 *
 * @param {*} user
 *
 * @returns boolean
 */
const userHasRegisteredEvent = (user) => {
  let bodyText = `Hello ${user.firstName}. Thanks for registering!`
  return mailer.sendEmailToUser(user.email, 'Welcome aboard!', bodyText)
}

module.exports = userHasRegisteredEvent

File: src/utils/mailer.js

/**
 * Sends an email to a user, if it was successfully sent, then return true, else return false
 *
 * @param {*} toEmail
 * @param {*} subject
 * @param {*} bodyText
 *
 * @returns boolean
 */
const sendEmailToUser = () => {
  return false
}

module.exports = {
  sendEmailToUser
}

7 - Response

Remember, our globalErrorHandler is able to catch all these ApiExceptions.

The first, DTO validation is triggered when we throw the following ApiException.

File: src/requests/registerUserDTO.js

throw new ApiException({
  status: 'error',
  code: 422,
  message: 'Input fields are of not the correct form.',
  data: errors
})
{
  "status": "error",
  "code": 422,
  "message": "Input fields are of not the correct form.",
  "data": null,
  "errors": [
    "This DTO's property is required: email.",
    "This DTO's property is required: password.",
    "This DTO's property is required: password_confirmation.",
    "This DTO's property is required: phone_number."
  ]
}

The second, form validation is triggered when we throw the following ApiException.

File: src/validators/registerUserValidator.js

throw new ApiException({
  status: 'error',
  code: 422,
  message: 'There were errors with the validation.',
  data: null,
  errors
})
{
  "status": "error",
  "code": 422,
  "message": "There were errors with the validation.",
  "data": null,
  "errors": [
    "The email format is invalid.",
    "The password confirmation and password fields must match."
  ]
}

The third, service layer validation in which we check if email already taken, is triggered when we throw the following ApiException.

File: src/domain/services/authService.js

/**
 * @returns User
 */
const registerUser = async (user) => {
  try {
    const createdUser = await userRepository.createUser(user)
    return createdUser
  } catch (err) {
    if (err.code === 11000 && err?.keyPattern?.email === 1) {
      throw new ApiException({
        message: `This email is already taken.`,
        status: 'error',
        code: 409,
        data: null,
        errors: [`This email is already taken.`]
      })
    }
  }
}
{
  "status": "error",
  "code": 409,
  "message": "This email is already taken.",
  "data": null,
  "errors": ["This email is already taken."]
}

Finally, the last response, which is the success response.

If everything goes through and there are no errors, then we will return a User object.

For that, we will use a DTO as follows.

File: src/responses/userResponseDTO.js

const userResponseDTO = (user) => {
  return {
    id: user['id'],
    first_name: user['first_name'],
    last_name: user['last_name'],
    email: user['email'],
    phone_number: user['phone_number']
  }
}

module.exports = userResponseDTO
{
  "status": "success",
  "code": 201,
  "message": "The email: yichenzhu13371@email.com has successfully registered.",
  "data": {
    "id": "61f889bbc6bbf81a97ba69d6",
    "first_name": "Yichen",
    "last_name": "Zhu",
    "email": "yichenzhu13371@email.com",
    "phone_number": "1234567890"
  },
  "errors": null
}

Putting It All Together

If you've followed a long, this is how our controller should look like by the end.

/**
 * Inserts the user into the database and fires off an email notification
 * to that user's email if successful.
 */
const registerUser = catchExceptions(async (req, res) => {
  // request
  const registerUserRequest = registerUserRequestDTO(req.body)

  // validation
  registerUserValidator(registerUserRequest)

  // domain logic
  const user = await authService.registerUser(registerUserRequest)

  // events
  eventEmitter.emit('userHasRegistered', user)

  // response - success
  return res.json(
    globalResponseDTO({
      status: 'success',
      code: 201,
      message: `The email: ${registerUserRequest.email} has successfully registered.`,
      data: userResponseDTO(user),
      errors: null
    })
  )
})
PreviousRegistering the User - PlanningNextLogging the User In - Planning

Last updated 3 years ago

Next is the form validation, we'll be using the to help us achieve the validation rules we specified in our planning section of this endpoint.

Notice here that we are not really going to be sending any real emails, we will just be stubbing it out. Of course, if you really wanted to, I suggest using or .

Recall in the previous section . We indicated that there were going to be 4 different responses.

Nice and clean .

😎
validatorjs library
SendGrid
Amazon SES
Registering the User - Planning