Welcome back!
Today we are going to start implementing authentication for the backend of our app "Gourmet".
In this post we will implement the sign up and verify endpoints.
Project steps
- Backend - Project Setup
- Backend - Authentication
- Backend - Place order
- Backend - View orders list and view a specific order
- Backend - Update order
- Frontend - Authentication
- Frontend - Place order, view orders list, and view order details
2. Backend - Authentication
Sign up
For users to register on our app, we will require their first name, last name, phone number, address, and password. If the values provided are valid, we will send a OTP (One-Time-Password/Passcode) via SMS to their phone number which they can use to confirm their registration.
Following the TDD approach, we are first going to write our tests then we will implement validations, middlewares, routes, controllers and finally we will configure Sequelize to be able to save data in the database.
Before we begin, ensure you have installed and configured Postgres properly as it's the database we are going to be using. Check out this article on how to install it on Ubuntu.
Our signup task is going to be made up of 2 sub-tasks, one for signing up and another for confirming the user's registration. Let's begin with the first one.
- Ensure you are on your
main
branch then rungit pull origin main
to make sure your local branch is up to date with the remote branch - Run
git checkout -b ft-authentication
to create a new branch for today's task
As we build our API there are things that we will need often and to avoid repeating ourselves it's good practice to structure our code for re-usability. That being said, create a new directory called utils
inside src
. Create two new files statusCodes.js
and messages.js
inside utils.
- Open
src/utils/statusCodes.js
and paste the following inside:
These are all the HTTP status codes that our API is going to use.
- Open
src/utils/messages.js
and paste the following inside:
This file will contain all the response messages that our API will be returning to the client apps on top of status codes and other data.
Now let's write our tests.
- Create a file called
authentication.test.js
in tests directory and paste the following inside:
In this file, we import our express app along with our assertion libraries (chai and chai-http) and our status codes and messages we defined above. We then define a base URL for our authentication routes and we initialize chai to be able to test http apps. Learn more about chai here.
We then define a SIGN UP
suite to hold our 5 test cases. In the first test case, we are testing for when a user submits an empty request (tries to signup without providing any data), what response he/she should get. Notice the use of one of our status code and messages we defined earlier.
In the second test case, we are testing for when a user submits an invalid phone number. Notice the missing +
sign on the phone number. The phone number must be in a valid international format since we will use it to send the OTP.
In the third test case, we are testing for when a user submits any other value apart from the required ones (firstName, lastName, phoneNumber, address, and password). Notice the email property.
In the fourth test case, we are testing for when a user submits valid values conforming to validation rules that we will define next. In this case, we expect a successful response that contains a status code of 201
, a account created
message, a JWT token that the user can use to authenticate for subsequent requests, and a data object containing details of the user. Notice how we expect the user's account status to be false since he/she has not yet verified it. Finally, we retrieve the token in a variable called userToken
that we will use in other test cases when verifying the user's account.
In the fifth test case, we are testing for when a user tries to signup more than once using the same phone number.
At this point if you run the tests they will fail apart from Server initialization test
which is exactly what we want.
Next up is to write code to make our tests pass.
Create the following directories
config
,controllers
,database
,helpers
,middlewares
,routes
,services
, andvalidations
insidesrc
directory.Create a new file called
authentication.js
inside validations directory and paste the following code inside:
We will use this file for authentication validation. In the code above, we start by importing a library called Joi
and our response messages we defined in utils. Joi is a powerful data validator for Javascript and I personally like it because it is robust and easy to use. Check out its docs here.
We created a function createErrorMessages
to help us in - you guessed it - create validation error messages. The function takes error type
and empty, min, max, and pattern
custom messages as parameters and depending on the type of the error we assign a custom message. This function returns an object of error types and their messages.
We use the second function signup
to define a schema of values that we want users to submit when signing up. Notice the use of Regular Expressions to enforce validation rules. If you're familiar with RegEx, it's pretty straight forward as our use-case is not too complex.
Finally we call Joi's built-in method validate
on our schema and pass in a data object i.e. req.body and some options to return all errors at once and to prevent other values not defined in our schema. Check out Joi API for more details and advanced use-cases.
In case of errors, our signup validation function will return an errors
object containing a details
property. This details property is an array containing all the error messages. We need a way to extract and use the contents of this details property.
- Create a
misc.js
file insidehelpers
directory and paste the following code:
In this file we define 3 functions:
We will use
successResponse
anderrorResponse
to return success and error responses respectively.returnErrorMessages
checks to see if the parametererrors
is present then destructure its details property. We then format each message in our details array to make it more readable and then we useerrorResponse
defined above to return the result of these formatted messages.
If errors is null it means our validations are passing and we continue with the execution of the request. Think of returnErrorMessages
as a middleware.
Let's now use this returnErrorMessages
function.
- Create a file
authentication.js
in middlewares directory and paste the following code:
Notice the use of returnErrorMessages
by giving it the error object returned by our signup validation function as a parameter.
Before we implement our controller, let's update src/helpers/misc.js
with the following:
Notice the additional functions: generateToken
, generateOTP
, and generateHashedPassword
.
We will use generateToken
to generate a JWT token based on the data passed in. Update your .env
file and include the JWT_SECRET_KEY
like JWT_SECRET_KEY=somesecretkey
.
We will use generateOTP
to generate a random six digit code that we will send to a user.
Finally, generateHashedPassword
will be used to take a plain-text password, encrypt it and return a hash string which we will store in our database. For security reasons, You should never store plain-text passwords in your database.
Okay, let's implement our controller.
- Create a
authentication.js
file incontrollers
directory and paste the following:
Our controller is where a request that has passed all validations and middlewares will end its journey. This where we will implement saving data in the database and sending OTP to users before returning a response to the user.
Let's implement our routes to see how it looks so far.
Create two files
authRoutes.js
andindex.js
in routes directory.Paste the following inside
src/routes/authRoutes.js
:
If you remember, in our tests we defined our base URL as /api/auth/
. This means we will be able to define /api/auth/signup
, /api/auth/login
, and /api/auth/logout
routes respectively.
Let's implement the parent /api/auth/
route handler.
- Paste the following inside
src/routes/index.js
:
Our endpoint is almost complete. We just need to let our express app know about it.
- Update
src/server.js
to look look like this:
- Run your tests again. This time around, some of them are passing.
Great Job if you managed to reach here! 🎉
Let's now implement sending OTP. When we finish, we will setup Sequelize in order to persist data in the database.
Starting with OTP implementation, we are going to use Twilio. Click here to create a Twilio trial account. After creating your account you should be given some credit you can use to buy numbers and send SMS in trial mode.
Trial accounts have some limitations, namely you cannot send SMS to unverified numbers. So in order to test this functionality, there are 2 options.
Option 1
You can upgrade your account.
Option 2
You can verify numbers you intend to use. Just remember to upgrade your account before going into production to allow everyone to signup.
We are going to use option 2 for now.
Login to your Twilio account. Click on the
#
sign that saysPhone numbers
on the left panel. On the phone numbers page, clickBuy number
button and proceed to search for a number you want. Make sure to tick the SMS checkbox.Click on
Verified Caller IDs
then click on the red plus button to add and verify a number. Make sure to provide a valid phone number that you have access to because Twilio will send a OTP to verify it.
Once done, head back to VS Code and add the following keys in your .env
file.
Let's now install the Twilio library.
Open your terminal in your project's root dir and run
yarn add twilio
Create a
twilioConfig.js
file in config directory and paste the following:
In this file we initialize a twilio client instance that we can use throughout our app to send SMS.
Let's now use this client in our code.
- Update
src/heplers/misc.js
to look like the following:
The sendOTP
function will take a phone number and a message and it will take care of sending our SMS. Let's now use this function in our controller.
- Update
src/controllers/authentication.js
like this:
Now run your tests again and you should get a OTP delivered to the number you specified in TWILIO_CUSTOMER_NUMBER
env variable.
Great! Let's now implement Sequelize and save data in our database.
Since we already installed all the required sequelize library and plugins, let's start using them.
- In your terminal, navigate to
src/database
and runnpx sequelize-cli init
. This command will create the following directories and files:config/config.json
,models
,migrations
, andseeders
.
The models directory will contain our models. Think of models as tables in a database.
The migrations directory will contain migrations which are modifications made to our models. We use migrations to change the structure our 'tables'. We can do things like add/remove/rename columns, add/change constraints on columns, etc.
Note that each time we modify the structure of our models we need to run migrations for those changes to take effect. More on this later.
The seeders
directory will contain data that we want to inject in the database. Use-case: Imagine you want to test the login
functionality. since we have already implemented the signup tests and we know it works well, we can use the seeders to insert in the database valid records of users thus skipping the signup and verify tests which will make our tests run faster. We will use seeders later in this series.
The config.json
file will contain credentials to connect to our database. We will need to modify this file and make it dynamic to avoid exposing our database credentials. Let's do it right away.
Rename
src/database/config/config.json
tosrc/database/config/config.js
Replace the contents inside with:
- Update your
.env
file and add the keys for development and test like below:
Notice the different database names for development and test.
Note that for now we don't need to provide credentials for production in our .env
file. The production credentials will be provided to us by heroku when we "provision" (setup) a production database.
- Replace
src/database/models/index.js
with the following:
This file will allow us to import our models dynamically by doing something like: import models from '../database/models'
then destructure models to retrieve each model in models directory. This file also creates and exports a sequelize instance that we will be using to interact with the database.
Cool! Let's now use Sequelize to create our first model - User.
- In your terminal run
npx sequelize-cli model:generate --name User --attributes firstName:string,lastName:string,phoneNumber:string,address:string
This command will create 2 new files: user.js
(our user model) and **-create-user.js
(our first migration) inside models and migrations directories respectively.
- Update
package.json
to include commands to create and drop the database as well as run the migrations like:
Notice we didn't include the pretest
command on the test
command since our CI service does this automatically for each build.
If we were to run our migrations right now, our database would be created with just the 4 columns defined when creating our model above.
Let's update our model and add more columns and create a new migration to apply those changes.
- Update
src/database/models/user.js
like below:
- In your terminal run
npx sequelize-cli migration:generate --name add-password-otp-and-status-to-user
to create a new migration that will apply the new columns we added to our model.
Tip: As migrations can become many as our app scales, it's good practice to name each migration with what it does. By looking at the name of the new migration, we would know that it add password, otp, and status columns to user model.
- Replace the contents of
src/database/migrations/**-add-password-otp-and-status-to-user.js
with the following:
Check out this link to learn more about creating models and migrations.
If we were to run our 2 migrations now, all the 7 columns would be added to our user table.
One of the things I like about Sequelize is its nice API that allows to interact with the database without writting SQL queries like "INSERT INTO tableName VALUES(....". Oh! This API allows also to write those queries in case you wish to use them. Nice, right!
We are almost done!
- Create a
services.js
file inside services directory and paste the following:
We will be using this file to create functions that will use Sequelize API to CRUD the database.
saveData
function receives a model name and obj as parameters then calls the Sequelize built-in method create
on the model and returns the data saved in the database.
Similarly we use findByCondition
function to find if a record exists in a table given a conditon. Check out this link to learn more about these built-in model methods.
As you might have guessed, we will use findByCondition
to check if a user exists in the database and saveData
to save the user.
Okay, let's update src/middlewares/authentication.js
to look like the following:
We need to run this function after the validations and before the controller.
- Update
src/routes/authRoutes.js
to look like:
- Lastly, let's update our controller to use the
saveData
function we defined in our services. Updatesrc/controllers/authentication.js
to look like the following:
In the code above we added the saveData
and lodash's omit
and pick
methods to choose which properties should be in the userData object returned in the response and token respectively.
That's it! Our signup endpoint is done!
Now if you run your tests, they should all pass! Nice, right!
In case you run into a timeout error, make sure to update your script's test command in package.json
by adding a timeout flag like below:
This allows to extend the default Mocha's timeout of 2 seconds for each test case to 8 seconds which will give enough time to our asynchronous functions to finish executing.
Verify
After users have registered and we have sent the OTP, we need a way to verify their phone number thus confirming their account registration.
We are going to implement verify endpoints, the first one will be to check if the OTP submitted by the user is correct. The second will be to re-send the OTP to the user in case there has been an issue and the user didn't receive the first OTP.
- Open
tests/authentication.js
and add the following:
In the code above, we have added test cases for the verify
and verify/retry
endpoints.
- In
SIGNUP
test suite, updateValid signup should return 201
test case like this:
- Open
src/utils/messages.js
and add the following messages:
- Open
src/validations/authentication.js
and add the following:
- Open
src/middlewares/authentication.js
and add the following:
- The
validateVerifyOTP
middleware will help us to useverifyOTP
function to validate theotp
submitted by the user. - The
checkUserToken
middleware will help us to check if a request contains the Authorization header then will try to decode the token to check if the who made the request exists in our database then returns the user's data or an error. This is how we will be able to link users with their requests. -
The
checkOTP
middleware will help us to check if the otp submitted by the user is the same as the one we sent to them via SMS.- Open
src/services/services.js
and add the following:
- Open
- Open
src/controllers/authentication.js
and add the following:
- Open
src/routes/authRoutes.js
and add the following:
Now all our tests should be passing. Let's now update our travis config file and package.json file before we commit our changes to Github.
- Update
.travis.yml
file to look like this:
We added the services
option and before_script
command which will tell Travis to create a postgres database called gourmet_test
before running our tests.
- Update
package.json
to include aheroku-postbuild
command.
As the name suggests, this command will run after each build. You can use it to run scripts that you want to execute before your app is deployed. Here we are using it to run our migrations automatically.
The last step is to make sure our CI service and production environments are up to date.
- Login on Travis then open our
gourmet-api
repo then click on settings to add environments variables. Make sure to add each env variable with its value.
- Head back to VS Code and commit our changes to github. Open a PR on github and wait for Travis to finish building. Both the branch and PR should show a successful build.
Before we merge this PR, let's create a production database on heroku.
On your app page on heroku, click on
Resources
tab then in theAdd-ons
search field typepostgres
. SelectHeroku Postgres
and in the confirmation modal clickSubmit order form
. You should see a confirmation that the add-onheroku-postgresql
has been added. Check out the docs for more info.Click on
Heroku Postgres
to open it in a new tab then click onSettings
tab then click theView credentials
button.
You should see the credentials of our database. When you provision a database on heroku like this, it adds the DATABASE_URL
env variable automatically on your app.
Let's now add the database credentials as env variables. Alternatively, you could use the DATABASE_URL
variable in the database/config/config.js
and database/models/index.js
files.
On your main app's settings tab, click on
Reveal config vars
button and add each credential key and its corresponding value from the database we've just created.Don't forget our Twilio credentials and JWT_SECRET_KEY
Now it's time to merge our PR which will trigger a production build on heroku.
- Head over to github and merge the PR we created earlier.
Travis should build our merge commit successfully then Heroku should build successfully as well then run our migrations.
Now you could copy the URL of your app from heroku and test the endpoints we implemented with POSTMAN or Insomnia and everything should go smoothly. Check out the links to their docs below.
Today's task was huge because we covered a lot of things. But we have laid the foundation for Sequelize, validations, and middlewares. The next endpoints are going to be fairly straightforward.
In the next post, we will implement the login
and logout
endpoints.
Tip: To test your API as you build it, you should use a tool like Postman or Insomnia.
They are both great at designing and testing APIs and you can even do things like creating and hosting your API documentation.
Check out the Postman docs and Insomnia docs to learn more.
Note: The endpoints we implemented in this post are a bit naive. For example, we are not checking if a user's account is verified before verifying it. We should also limit requests to the endpoints that uses external resources since the billing of these resources can become a lot. Check out this library to learn how to limit the number of requests. About the other issue of checking if a user's account is verified before verifying it, we can achieve this by using a simple middleware function.
Thanks for reading and or following along!
See you in the next one!
You can find the code in this post here
Top comments (2)
my src folder gets hidden whenever i switch to main branch
so i don't know how i can create a new folder in the src