In this guide, we are going to build a REST API with Node.js, TypeScript, and MongoDB. This guide is for junior to mid-level developers that want to build APIs like a senior developer.
Finished repository: https://github.com/TomDoesTech/REST-API-Tutorial
Note: This guide uses commands like
touch. For those that are unfamiliar,
mkdir will make a new directory and
touch will create a new file. These commands will work on a Mac or Linux OS, but will not work on a Windows machine as far as I’m aware.
Note 2: For the best learning outcome, watch the video above and follow along. Use this article for support when you miss a snippet of code.
Technologies and concepts covered:
- REST APIs
- JWTs & refresh tokens
- MongoDB with Mongoose
Why should you learn how to build a REST API in 2021? Isn’t it all about GraphQL now?
- REST APIs are still a staple of the web development industry
- Microservices architectures take advantage of REST
- You need to know how REST APIs work even if you’re a front-end developer
Bootstrap the application
Start in an empty directory and create and initialize a new Node.js application
Initialize the TypeScript project
npx typescript --init
nodemon.json file and add the following code:
package.json file to include a run script:
This script will run and watch the
Install some required dependencies:
yarn add express yup config cors express mongoose pino pino-pretty dayjs bcrypt jsonwebtoken lodash nanoid
Add some development dependencies:
yarn add @types/body-parser @types/config @types/cors @types/express @types/node @types/yup @types/pino @types/mongoose @types/bcrypt @types/jsonwebtoken @types/lodash @types/nanoid ts-node typescript -D
src directory and add a file called
mkdir src && touch /src/app.ts
Create a config file:
mkdir config && touch /config/default.ts
/config/default.ts and add the following object:
/src/app.ts and add the following:
We can now start the application with
yarn dev and see the server start on port
Configure the logger
Using a logging module over
console.loggives us more control over how the logs are formatted and when we get to production, what happens to those logs. For this guide, we will use Pino, but you can use any logging module you like.
Create a new file for the logger:
mkdir src/logger && touch src/logger/index.ts
Add the following code to the file:
Play around with the format options to find a format that you like.
Configure the Mongoose database connection
Create a new file for the database connection function:
mkdir src/db && touch src/db/connect.ts
Add the following code to the file:
Configure the routes file
Create a new file for our routes:
Add the following code to the routes file:
Bring it all together
Now that we have the logger, database connection, and routes configured, it’s time to bring it all together in the
- Import the logger, routes, and connect function.
- Update the
console.logstatement to use
- Call the connect function inside the
- Call routes inside the
app.listencall and pass in the instance of the app
app.ts file should now look like the following:
For the purposes of this guide, the user registration flow is going to be a simplistic version of what you would have in a production-grade application. However, this will get the basics in for you to expand on.
Create user model
mkdir src/model && touch src/model/user.model.ts
Create user service
mkdir src/service && touch src/service/user.service.ts
Create user controller
mkdir src/controller && touch src/controller/user.controller.ts
Create session model
Create session service
Create session controller
Create a user
Create an endpoint in the routes file to handle the user registration
app.post(“/api/users”, validateRequest(createUserSchema), createUserHandler);
You’ll notice the
validateRequest between the endpoint and the handler function. This is Express.js middleware. In this case, we have a function that takes a Yup validation schema that will validate the request body.
Create a folder for the middleware and the
mkdir src/middleware && touch validateRequest.ts
Import the validate request middleware into the routes file.
Create a schema file for all the user-related validation schemas:
mkdir src/schema && touch src/schema/user.schema.ts
createUserHandler to the controller:
createUser service into the controller, then import the controller into the routes file and you can test the handler by sending a
POST request to
Create a user session
Add the create user session endpoint to the routes file:
app.post("/api/sessions", validateRequest(createUserSessionSchema), createUserSessionHandler);
Add a handler for creating a session:
Add the services for creating a session:
Create a utility function for signing JWTs:
mkdir utils && touch utils/jwt.utils.ts
refreshTokenTtl to your config file:
You can use the private key in the example above, or generate your own private key.
Delete a user session
app.delete(“/api/sessions”, requiresUser, invalidateUserSessionHandler);
The delete session endpoint includes some middleware called
requiresUser. Create this middleware in the
requiresUser middleware checks that the user is available on the Express.js request object. If it is not, the request will return a 403 error. The user gets onto the request object with another bit of middleware.
Create a file in the middleware folder called
The above code will check to make sure the user’s access token is valid, if it isn’t it will check to see if it has expired. If the access token has expired, it will try to use the
refreshToken to generate a new access token.
This allows us to keep the application stateless while the user has a valid access token. We have configured the access token to last for 15 minutes. It also allows us to revoke the user’s access if needed. If the user’s access is not revoked, their session will last for a year.
We want this middleware to run on every request. To do that, we call
app.use and pass it our middleware:
You’ll notice the middleware is imported from
./middleware and not
To allow these types of imports, create a new file inside the middleware directory:
Add the following code to the file:
This nifty file allows us to import our middleware from a single file as named imports.
Get all sessions for the currently logged in user
By getting all the sessions, the user has an audit trail they can use to see where there and when their account has been accessed. To extend this functionality, you can allow the user to set the valid property on each session to false, removing the ability for the
refreshToken to be used to grant another
Add the following route to the routes file:
app.get(“/api/sessions”, requiresUser, getUserSessionsHandler);
Create the handler:
Post CRUD operations
Create post model
The post model is similar to the user and session models. However, it includes a call to
nanoid, which will be used to generate a shorter ID. Instead of nanoid, you could use the post title to generate a unique slug.
Create post service
Create post controller
Create a post
app.post(“/api/posts”, [requiresUser, validateRequest(createPostSchema)], createPostHandler);
The create post endpoint uses multiple pieces of middleware. Express.js allows us to do this by providing the middleware in an array.
Read a post
Add the read post handler:
Update a post
app.put(“/api/posts/:postId”, [requiresUser, validateRequest(updatePostSchema)], updatePostHandler);
Add update post handler:
Delete a post
app.delete(“/api/posts/:postId”, [requiresUser, validateRequest(deletePostSchema)], deletePostHandler);
Add delete post handler:
Add delete post service: