What we are building
Introduction
ExpressJS Web API with JWT full Auth System
In this article, we will walk through the full process of building an ExpressJS API with JWT Authentication system, and E2E testing. We will be building this API with:
-
NodeJS
-
ExpressJS
-
TypeScript
-
PrismaJS
-
PostgresSQL (Your choice)
-
Jest
-
NodeMailer
-
ZOD We will also use other packages to achieve certain goals.
Architecture
Before we dive in code, This is what we will be building. The 5 main process of an Auth System:
Login Process: Simple JWT powered login process. the Basic process,
jsonwebtoken
package handles the process of JWT behind the scene for us. Except the Refresh Token, We generate it and store it.
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1722102599137/2eebe303-71eb-4ad5-94bf-63530e8d47a3.png align="left")
Refresh Token Process: After the token is expired, the client side should call this API Endpoint with the refresh token provided at login.
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1722102602157/7b338bd2-6cae-459b-b8e6-5f8c0fa2409a.png align="left")
Register Process: This process have configured process, we have the process of requiring Email Verification, and without. In the API we will be building, I added this configuration to a config file. This process is quite simple too. We send the new users payload from client, The API checks the request body schema with
ZOD
and checks duplicated emails. If all is good, We create the new User in DB and return theUserObject
. If the Email Verification if toggled on we generate a token and store it, then send an Email to the client with the token attached to the client's URL responsible for that process. After the user clicks the URL with the token, the client side sends the token inverify-user
endpoint. The API Then validates the token, update the UserverifiedEmail
property, and delete the token from DB. And return astatus
response if everything is good. Also, This token has an Expiration Date (Currently set to 1 hour).
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1722102614477/c17eee04-e753-4426-9e6f-836ce7672148.png align="left")
Reset Password Process: This process has two steps; The Forget Password triggering and the verification of Email, Then the reset password process. In the Forget Password step, The API receives the Email and Type (This type is business related property, For example we have many types of Users (ADMIN, User, Client...)). In this step, we only send the token in Email. This token is generated locally and stored in DB to validate it later. The Reset Process is triggered when the user clicks the link, goes to client side page and fill the Resetting password form. The new password is attached with the token and sent to API. After receiving the payload, we validate the token, update the password and finally delete the token from DB.
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1722102620227/acee33ef-f266-402d-9532-c3653821edfb.png align="left")
Update Password Process: This process is the only one in Auth System that requires the JWT Access Token provided in Login. We have two ways to update password (Configurable in the API as well). We have the simple one, Where we update directly the password, and the guarded one, where we only update after the user confirms the action through mail token. The simple way is simple as its name indicates. We just update the password, the access token is a way to confirm the user's identity (But not enough). Guarded way has another step for security check. Where we store the new password (Hashed of course), and add a token to that table. We send the token through email. After getting the token, The client sends it through
confirm-update-password
endpoint. We validate the token, if there is a match and it's not expired, we update the user's password with the stored one. Then delete the token record and return a success status.
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1722102624084/8633e22d-5f7c-46a3-824c-4575ecb7ad64.png align="left")
Now we understand what we are building and how we build it. This Project is simple one, But the process of designing and engineering is required process to have a good, scalable, and clean product. You can apply the same methodology (It will vary depending on the product/system) to build more robust products and also train your brain to see Development in Design and Diagrams more than code in screen.
Requirements
To follow along smoothly this article you will need:
-
NodeJS
-
Knowledge of NodeJS, ExpressJS and PrismaJS
-
Knowledge of TypeScript
-
Postman (For testing -- optional)
Setup
You will find the full code source in GitHub
We will first install the basic requirements to setup ExpressJS and PrismaJS. Create the project's directory and initialize NPM:
In the directory run the next NPM install scripts:
Also the types and devDependencies
:
Extra ExpressJS Third-Party Middlewares and clean code:
All in one:
Code Structure
The app will roughly follow this approach. We separate the code by domain; Router handlers, Server Handlers, Repository Handlers and Services Dependencies.
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1722102634186/607c84b1-1ca3-4050-9d25-30adad144438.png align="left")
Now, initialize PrismaJS:
Create tsconfig.json
file and paste this code:
This script will create prisma
directory and the schema. Plus, .env
file with DATABASE_URL
variable.
The folder structure we gonna create now:
The prisma
folder will be generated automatically. You will find the ESLint and Prettier configs in the GitHub Repo. Just paste them.
In package.json
, Add these scripts:
Setting up ExpressJS
The app.ts
file will handle all ExpressJS setup and code. And server.ts
will only call it and listen to it. In app.ts
, we will follow this structure:
-
Initialize Express App
-
Setup Middlewares
-
Handle Routes
-
Handle Not Found Route
-
Disconnect Prisma instance (will be handled later after setting up prisma service)
-
Export the Express App
This code uses some functions and dependencies we haven't created yet. Let's create them.
App Config
This file will handle the configs of the API and import the environment variables. Create a file named app.config.ts
in src/config/
directory.
We will need also to add those environment variables to .env
file:
Response Handler
The purpose of this handler is to manage and unify the API response bodies. This handler depends also on HTTPStatusCodes.ts
, Which is just an enumerator of HTTP status codes.
In src/utils/
create a file named HTTPStatusCodes.ts
and paste the content from GitHub repo file ATTACHED. Also in the same directory, create a file named responseHandler.ts
. This file will include two classes.
This ApiResponseBody
will be used like this:
This code will return:
We can also use the ResponseHandler
class like this:
Prisma Service
At the end of Express App, we disconnect prisma. The prisma service is a singleton for prisma client, it also handles the Test DB environment.
In src/services/
create a file named prisma.service.ts
.
Winston Logs
I used Winston to handle the logs and write them in log files. Winston is a good package for this. It creates multi-channels for logs and enable creating files for each level, and also print out in console. First, install it:
Then, create winston.ts
in src/utils
.
Routes
Now we handles the routes. And for API versioning, We will create an index.ts
file in src/routes/v{version}
. That file will handle the routes for that version.
With that being said, Let's create the index file in src/routes/v1/index.ts
The router index will call all the routes for v1
.
This router calls the auth.route.ts
and uses JWT middleware for the protected API endpoint.