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. */constregisterUser=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
constApiException=require('../utils/ApiException')constfields= ['first_name','last_name','email','password','password_confirmation','phone_number']/** * @param Object data */constregisterUserRequestDto= (data) => {consterrors= []fields.forEach((field) => {if (!(field in data)) {errors.push(`This DTO's property is required: ${field}.`) } })if (errors.length>0) {thrownewApiException({ 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.
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
constValidator=require('validatorjs')constApiException=require('../utils/ApiException')/** * @param{*} data { * - first_name * - last_name * - email * - password * - password_confirm * - phone_number * } * * @returns Validator */constregisterUserValidator= (data) => {construles= { first_name:'required', last_name:'required', email:'required|email', password:'required|min:6', password_confirmation:'required|min:6|same:password', phone_number:'required|telephone' }constvalidator=newValidator(data, rules)if (validator.fails()) {let errors = []for (constfieldinvalidator.errors.errors) { errors =errors.concat(validator.errors.errors[field]) }thrownewApiException({ status:'error', code:422, message:'There were errors with the validation.', data:null, errors }) }return validator}// This is our custom 'telephone' validation ruleValidator.register('telephone',function (value) {returnvalue.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.
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.
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 */constsendEmailToUser= () => {returnfalse}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
thrownewApiException({ 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
thrownewApiException({ 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.
{"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. */constregisterUser=catchExceptions(async (req, res) => {// requestconstregisterUserRequest=registerUserRequestDTO(req.body)// validationregisterUserValidator(registerUserRequest)// domain logicconstuser=awaitauthService.registerUser(registerUserRequest)// eventseventEmitter.emit('userHasRegistered', user)// response - successreturnres.json(globalResponseDTO({ status:'success', code:201, message:`The email: ${registerUserRequest.email} has successfully registered.`, data:userResponseDTO(user), errors:null }) )})