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

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

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

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 SendGrid or Amazon SES.

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

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

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
    })
  )
})

Last updated