How to Develop Simple CRUD API in Express.js using TypeScript

Note: This post is part of Learn How to Use TypeScript With Node.js and Express.js series. Click here to see the first post of the series.

What is CRUD?

CRUD stands for Create, Read, Update, and Delete and it is one of the most common operations done in software development. In fact, it is one of the initial steps for new programmers to make an application interact with a database.

Developing Simple CRUD API

For now, we are going to start by developing a simple CRUD for our API. If you’ve been following the Learn How to Use TypeScript With Node.js and Express.js series, you will know we already have a GET Teams API endpoint defined in the teams.routes.ts like this:


import { Router } from 'express';
import { getTeams } from './teams.controller';
import * as Auth from './../middlewares/auth.middleware';
 
const router = Router();
 
router.route('/').get(
  Auth.authorize(['getTeams']),
  getTeams
);
 
export default router;

Which triggers the getTeams function handler from the teams.controller.ts file with the following logic:


import { Request, Response } from 'express';
 
const TEAMS = [
  { id: 1, name: 'Real Madrid', league: 'La Liga' },
  { id: 2, name: 'Barcelona', league: 'La Liga' },
  { id: 3, name: 'Manchester United', league: 'Premier League' },
  { id: 4, name: 'Liverpool', league: 'Premier League' },
  { id: 5, name: 'Arsenal', league: 'Premier League' },
  { id: 6, name: 'Inter', league: 'Serie A' },
  { id: 7, name: 'Milan', league: 'Serie A' },
  { id: 8, name: 'Juventus', league: 'Serie A' },
];
 
export const getTeams = (req: Request, res: Response) => {
  res.send(TEAMS);
};

In the meantime, we are not planning to connect a database as the main emphasis in this section will be to leverage the power of TypeScript.

1. Defining CRUD Interfaces

First things first, we need to take advantage of TypeScript to make development easy. If you have developed APIs using Express.js with Node.js in the past, you will know there is no way to tell what kind of values the client could provide in a request via:

  • Parameters
  • Query parameters
  • Body

However, our req argument will not know any of that information as by default we are using the default Request interface provided by express. To make development easier, let’s start via setting up interfaces that could be used for our CRUD operations.

Create a teams.model.ts file inside the src/api/teams folder. There we will import the Request interface so we can inherit it.

Now, it is time for us to set the interfaces used as the requests obtained once parameters, query parameters and body data is parsed in order to fetch, add, update, and delete team data.


import { Request } from 'express';
 
export interface IGetTeamReq extends Request{}
export interface IAddTeamReq extends Request{}
export interface IUpdateTeamReq extends Request{}
export interface IDeleteTeamReq extends Request{}

It seems we are not doing anything at the moment besides creating child interfaces of the Request interface. However, if you hover over the Request interface using your IDE, you will see the types used for the request parameter, request query parameter, and request body interfaces. These are called generics in TypeScript. This allows us to reuse the interface over and over while updating the types defined based on our logic.

You should see the following options:

// <P = core.ParamsDictionary, 
// ResBody = any, 
// ReqBody = any, 
// ReqQuery = qs.ParsedQs, 
// Locals extends Record<string, any> = Record<string, any>>

Therefore

  • If we update the 1st generic, we will update the request parameters type
  • If we update the 2nd generic, we will update the response body type
  • If we update the 3rd generic, we will update the request body type
  • If we update the 4th generic, we will update the request query parameters type

Therefore, we are going to update the interfaces with interfaces that best suit our project needs.


import { Request } from 'express';
 
export interface ITeam {
  id: number;
  name: string;
  league: string,
  isActive: boolean
};
 
export interface IGetTeamReq extends Request<{ id: ITeam['id'] }> { }
export interface IAddTeamReq extends Request { }
export interface IUpdateTeamReq extends Request<{ id: ITeam['id'] }, any, ITeam> { }
export interface IDeleteTeamReq extends Request<{ id: ITeam['id'] }> { }

Notice how we added a isActive property flag to the ITeam interface. We will use it to determine soft delete rather than hard delete the team record.

2. Update Data to Use isActive flag

Currently, our local data doesn’t contain isActive flag. Let’s open the teams.controller.ts and add isActive property to the team objects. Also, let’s import the ITeam interface to add the type definition to the objects.


import {
  ITeam
} from './teams.model';
 
const TEAMS: ITeam[]  = [
  { id: 1, name: 'Real Madrid', league: 'La Liga', isActive: true },
  { id: 2, name: 'Barcelona', league: 'La Liga', isActive: true },
  { id: 3, name: 'Manchester United', league: 'Premier League', isActive: true },
  { id: 4, name: 'Liverpool', league: 'Premier League', isActive: true },
  { id: 5, name: 'Arsenal', league: 'Premier League', isActive: true },
  { id: 6, name: 'Inter', league: 'Serie A', isActive: true },
  { id: 7, name: 'Milan', league: 'Serie A', isActive: true },
  { id: 8, name: 'Juventus', league: 'Serie A', isActive: true },
];

3. Defining CRUD Function Handlers

Now we are going to generate function handlers use for each CRUD operation. Therefore, we need to import all the request interfaces previously prepared all CRUD operations as well as adding the necessary logic.

/**
 * Get team record based on id provided
 *
 * @param req Express Request
 * @param res Express Response
 */
// @ts-ignore
export const getTeamById: RequestHandler = (req: IGetTeamReq, res: Response) =&gt; {
  const team = TEAMS.find((team) =&gt; team.id === +req.params.id &amp;&amp; team.isActive);
  res.send(team);
};
 
/**
 * Inserts a new team record based 
 *
 * @param req Express Request
 * @param res Express Response
 */
export const addTeam: RequestHandler = (req: IAddTeamReq, res: Response) =&gt; {
  const lastTeamIndex = TEAMS.length - 1;
  const lastId = TEAMS[lastTeamIndex].id;
  const id = lastId + 1;
  const newTeam: ITeam = {
    ...req.body,
    id,
    isActive: true
  };
 
  TEAMS.push(newTeam);
 
  res.send(newTeam);
};
 
/**
 * Updates existing team record
 *
 * @param req Express Request
 * @param res Express Response
 */
// @ts-ignore
export const updateTeamById: RequestHandler = (req: IUpdateTeamReq, res: Response) =&gt; {
  const currentTeam = TEAMS.find((team) =&gt; team.id === +req.params.id &amp;&amp; team.isActive);
  currentTeam.name = req.body.name || currentTeam.name;
  currentTeam.league = req.body.league || currentTeam.league;
 
  res.send({ success: true });
};
 
/**
 * deletes a team
 *
 * @param req Express Request
 * @param res Express Response
 */
// @ts-ignore
export const deleteTeamById: RequestHandler = (req: IDeleteTeamReq, res: Response) =&gt; {
  const teamIndex = TEAMS.findIndex((team) =&gt; team.id === +req.params.id &amp;&amp; team.isActive);
  TEAMS.splice(teamIndex, 1);
  
  res.send({ success: true });
};

Note: Notice I imported the RequestHandler type from express. We will need to use it to avoid any errors when defining the routes. Also, we are disabling any potential errors caused by not using the ParamsDictionary in some request interfaces and rather using a custom object such as { id: ITeam['id'] } to define the expected req.params object for a specific endpoint.

Let’s go ahead an update the logic of the getTeams function handler to retrieve only active team records.


/**
 * Get active team records
 *
 * @param req Express Request
 * @param res Express Response
 */
export const getTeams: RequestHandler = (req: Request, res: Response) => {
  const activeTeams = TEAMS.filter((team) => team.isActive);
  res.send(activeTeams);
};

4. Define API Endpoints

Open the team.routes.ts file, import the function handlers previously defined in the controller, and define the API endpoints for all the CRUD operations.


import { Router, } from 'express';
import * as Controller from './teams.controller';
import * as Auth from './../middlewares/auth.middleware';
 
const router = Router();
 
router
  .route('/')
  .get(
    Auth.authorize(['getTeams']),
    Controller.getTeams
  );
 
router
  .route('/:id')
  .get(
    Auth.authorize(['getTeams']),
    Controller.getTeamById
  );
 
router
  .route('/')
  .post(
    Auth.authorize(['addTeams']),
    Controller.addTeam
  );
 
router
  .route('/:id')
  .patch(
    Auth.authorize(['updateTeams']),
    Controller.updateTeamById
  );
 
router
  .route('/:id')
  .delete(
    Auth.authorize(['deleteTeams']),
    Controller.deleteTeamById
  );
 
export default router;

5. Test Endpoints

Now that we have generated the teams CRUD API endpoint, it is time to do some testing and verify everything is working as expected.

What’s Next?

You have learned how to develop a CRUD API in Express.js using TypeScript. However, we know data needs to be stored in a database instead of in-memory. In the next article, we are going to learn how to develop CRUD API using MySQL database.