Create a REST API with Node.js, Express, MongoDB and Typescript

Photo by Tetiana SHYSHKINA on Unsplash
Photo by Tetiana SHYSHKINA on Unsplash

Photo by Tetiana SHYSHKINA on Unsplash

REST is the acronym for REpresentational State Transfer. It is an architectural style that defines a set of rules to create Web Services. It was presented for the first time by Roy Fielding in 2000 in his dissertation.

A REST API is an Application Programming Interface that follows the REST architecture constraints.

There are 6 principles to follow to build a REST API:

  • Client-Server
  • Stateless
  • Cacheable
  • Uniform Interface
  • Layered System
  • Code on Demand

To learn more about REST and its principles, check out this link.

Prerequisites

To follow this tutorial, you need to have these tools installed on your computer:

  • Node.js 12+
  • NPM or Yarn (I will use Yarn)
  • MongoDB installed; Download the version for your operating system.
  • A GUI REST client; I will use Postman

Set up the project

To start, we will use a boilerplate for the Node.js project we built on this tutorial. The main branch contains a simple Node.js application.

The branch express-mongo contains a project with the Framework express and the connection to a Mongo database already configured.

Check this tutorial to see how to connect Node.js with MongoDB.

git clone https://github.com/tericcabrel/node-ts-starter.git -b express-mongo node-rest-api

cd node-rest-api

cp .env.example .env

nano .env

yarn install

yarn start
Initialize the project with minimal code

Now we have a working project, we will continue by creating the Mongo schema representing the data structure we want to store in the database.

What we will create is a classic users management app where we can create roles, create users with a role and also change the role of the user. A user can have a role and only one at a time.

Create the schema

We will create two schemas, respectively, for role and user. Inside the folder src, create a folder named models then create a file role.model.ts and finally, add the code below:

import mongoose, { Schema, Model, Document } from 'mongoose';

type RoleDocument = Document & {
  name: string;
  description: string | null;
};

type RoleInput = {
  name: RoleDocument['name'];
  description: RoleDocument['description'];
};

const roleSchema = new Schema(
  {
    name: {
      type: Schema.Types.String,
      required: true,
      unique: true,
    },
    description: {
      type: Schema.Types.String,
      default: null,
    },
  },
  {
    collection: 'roles',
    timestamps: true,
  },
);

const Role: Model<RoleDocument> = mongoose.model<RoleDocument>('Role', roleSchema);

export { Role, RoleInput, RoleDocument };
src/models/role.model.ts

Create user.model.ts and add the code below:

import mongoose, { Schema, Model, Document } from 'mongoose';

type UserDocument = Document & {
  fullName: string;
  email: string;
  password: string;
  enabled: string;
  role: string;
};

type UserInput = {
  fullName: UserDocument['fullName'];
  email: UserDocument['email'];
  password: UserDocument['password'];
  enabled: UserDocument['enabled'];
  role: UserDocument['role'];
};

const usersSchema = new Schema(
  {
    fullName: {
      type: Schema.Types.String,
      required: true,
    },
    email: {
      type: Schema.Types.String,
      required: true,
      unique: true,
    },
    password: {
      type: Schema.Types.String,
      required: true,
    },
    enabled: {
      type: Schema.Types.Boolean,
      default: true,
    },
    role: {
      type: Schema.Types.ObjectId,
      ref: 'Role',
      required: true,
      index: true,
    },
  },
  {
    collection: 'users',
    timestamps: true,
  },
);

const User: Model<UserDocument> = mongoose.model<UserDocument>('User', usersSchema);

export { User, UserInput, UserDocument };
src/models/user.model.ts

Create a resource for the Role entity

Following the REST specification, we will have these routes:

  • Create an roles: [POST] /roles
  • Get all roles: [GET] /roles
  • Get one user: [GET] /roles/:id
  • Update a role: [PUT] /roles/:id
  • Delete a role: [DELETE] /roles/:id

Inside the folder src, create a folder named routes then create a file role.route.ts. This file will contain the route configuration listed above.

import { Router } from 'express';

const roleRoute = () => {
  const router = Router();

  router.post('/roles', (req, res) => {
    // TODO logic for creating role
  });

  router.get('/roles', (req, res) => {
    // TODO logic for retrieving roles
  });

  router.get('/roles/:id', (req, res) => {
    // TODO logic for retrieving role
  });

  router.put('/roles/:id', (req, res) => {
    // TODO logic for updating role
  });

  router.delete('/roles/:id', (req, res) => {
    // TODO logic for deleting role
  });

  return router;
};
src/routes/role.route.ts

We defined all the endpoints and let's add the logic that will be executed for each endpoint.

Separation of concerns

We can write the logic in the route file, but it is good to separate the route configuration from the logic related to these routes for a better separation of concerns in our project. By doing that, we have a code more readable and maintainable.

Inside the folder, create a folder named controllers then create a file role.controller.ts. This file will contain the logic for all role endpoints.

Create a role

import { Request, Response } from 'express';
import { Role, RoleInput } from '../models/role.model';

const createRole = async (req: Request, res: Response) => {
  const { description, name } = req.body;

  if (!name || !description) {
    return res.status(422).json({
      message: 'The fields name and description are required',
    });
  }

  const roleInput: RoleInput = {
    name,
    description,
  };

  const roleCreated = Role.create(roleInput);

  return res.status(201).json({ data: roleCreated });
};

export { createRole };

The logic for creating a role is ready to update the route configuration file:

Replace the code below:

router.post('/roles', (req, res) => {
  // TODO logic for creating role
});

By:

import { createRole } from '../controllers/role.controller';
...
...
...
router.post('/roles', createRole);

Retrieve all roles

const getAllRoles = async (req: Request, res: Response) => {
  const roles = await Role.find().sort('-createdAt').exec();

  return res.status(200).json({ data: roles });
};

We retrieve all the roles and sort them by date of creation in descending order (the most to the least recently created role). Also, update role.route.ts as well.

Retrieve one role

const getRole = async (req: Request, res: Response) => {
  const { id } = req.params;

  const role = await Role.findOne({ _id: id });

  if (!role) {
    return res.status(404).json({ message: `Role with id "${id}" not found.` });
  }

  return res.status(200).json({ data: role });
};

We retrieve the role by the id passed as a parameter in the URL roles/:id. If the role is not found, we return a message; otherwise, the role's object found.

Update role

const updateRole = async (req: Request, res: Response) => {
  const { id } = req.params;
  const { description, name } = req.body;

  const role = await Role.findOne({ _id: id });

  if (!role) {
    return res.status(404).json({ message: `Role with id "${id}" not found.` });
  }

  if (!name || !description) {
    return res.status(422).json({ message: 'The fields name and description are required' });
  }

  await Role.updateOne({ _id: id }, { name, description });

  const roleUpdated = await Role.findById(id, { name, description });

  return res.status(200).json({ data: roleUpdated });
};

Delete role

const deleteRole = async (req: Request, res: Response) => {
  const { id } = req.params;

  await Role.findByIdAndDelete(id);

  return res.status(200).json({ message: 'Role deleted successfully.' });
};

At this step, here is what role.route.ts looks like:

import { Router } from 'express';
import { createRole, deleteRole, getAllRoles, getRole, updateRole } from '../controllers/role.controller';

const roleRoute = () => {
  const router = Router();

  router.post('/roles', createRole);

  router.get('/roles', getAllRoles);

  router.get('/roles/:id', getRole);

  router.put('/roles/:id', updateRole);

  router.delete('/roles/:id', deleteRole);

  return router;
};

export { roleRoute };

For now, if you start the app and call an endpoint, you will receive a 404 Not Found  because the routing configuration is not registered to Express yet. To do that, in the file, src/index.ts add the code below after app.use(express.json());:


app.use('/', roleRoute());

Inside src/index.ts

Test our REST API

It's time to test what we have done until now. Start the app with yarn start. To test our REST API, we will use an awesome tool called Postman.

Download and install and make your first call ?

Using Postman to create a role

Create two other roles with the name USER and GUEST then make the call to retrieve all the roles.

Using Postman to retrieve all roles

As you can see, the roles are sorted as expected.

Create User's Resource

Here is the code of src/controllers/user.controller.ts

import { Request, Response } from 'express';
import crypto from 'crypto';

import { User, UserInput } from '../models/user.model';

const hashPassword = (password: string) => {
  const salt = crypto.randomBytes(16).toString('hex');

  // Hashing salt and password with 100 iterations, 64 length and sha512 digest
  return crypto.pbkdf2Sync(password, salt, 100, 64, `sha512`).toString(`hex`);
};

const createUser = async (req: Request, res: Response) => {
  const { email, enabled, fullName, password, role } = req.body;

  if (!email || !fullName || !password || !role) {
    return res.status(422).json({ message: 'The fields email, fullName, password and role are required' });
  }

  const userInput: UserInput = {
    fullName,
    email,
    password: hashPassword(password),
    enabled,
    role,
  };

  const userCreated = await User.create(userInput);

  return res.status(201).json({ data: userCreated });
};

const getAllUsers = async (req: Request, res: Response) => {
  const users = await User.find().populate('role').sort('-createdAt').exec();

  return res.status(200).json({ data: users });
};

const getUser = async (req: Request, res: Response) => {
  const { id } = req.params;

  const user = await User.findOne({ _id: id }).populate('role').exec();

  if (!user) {
    return res.status(404).json({ message: `User with id "${id}" not found.` });
  }

  return res.status(200).json({ data: user });
};

const updateUser = async (req: Request, res: Response) => {
  const { id } = req.params;
  const { enabled, fullName, role } = req.body;

  const user = await User.findOne({ _id: id });

  if (!user) {
    return res.status(404).json({ message: `User with id "${id}" not found.` });
  }

  if (!fullName || !role) {
    return res.status(422).json({ message: 'The fields fullName and role are required' });
  }

  await User.updateOne({ _id: id }, { enabled, fullName, role });

  const userUpdated = await User.findById(id);

  return res.status(200).json({ data: userUpdated });
};

const deleteUser = async (req: Request, res: Response) => {
  const { id } = req.params;

  await User.findByIdAndDelete(id);

  return res.status(200).json({ message: 'User deleted successfully.' });
};

export { createUser, deleteUser, getAllUsers, getUser, updateUser };
Controller for User

Code for user's route configuration

import { Router } from 'express';
import { createUser, deleteUser, getAllUsers, getUser, updateUser } from '../controllers/user.controller';

const userRoute = () => {
  const router = Router();

  router.post('/users', createUser);

  router.get('/users', getAllUsers);

  router.get('/users/:id', getUser);

  router.patch('/users/:id', updateUser);

  router.delete('/users/:id', deleteUser);

  return router;
};

export { userRoute };
Route configuration for User

Finally, register the route configuration in Express by adding the code below in src/index.ts


app.use('/', userRoute());

Create a user with Postman

Using Postman to create a user with a Super Admin role

Patch vs. Put Method

For updating role and user, we used the method PUT for the former and PATCH for the latter. For a role, we update all the properties of the model, while on the user, we only update some properties of the model.

PUT verb should be used when updating the entire object and PATCH when we partially update the object.

Conclusion

We reached the end of this tutorial, where we built a REST API with Node.js.

You can find the final code here. I also included the Postman collection with all the endpoints we have created throughout this tutorial ?.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.