Building a REST API with Typescript and Express

Building a REST API with Typescript and Express

Get started with building scalable Node.js projects using Typescript.

Maybe you have spent countless hours trying to fix a bug when writing Javascript code only to discover it was just a simple typo, It can be frustrating but Typescript can help.

Typescript is a strongly typed programming language and a superset of Javascript which helps you add static type checking to your JavaScript code. By adding static type checking, TypeScript helps you detect bugs even before you execute your code and adds autocompletion for supported IDEs which makes development faster.

For example, VS Code implements autocompletion for Javascript and Typescript code using its inbuilt TypeScript compiler. In this article, we will cover how to set up an express application with Typescript and also build a simple CRUD express application to familiarize you with Typescript.

To follow along in this tutorial properly, be sure to have the following:

  • Node.js version v14 or more installed on your local computer.

  • Basic knowledge of Express and Node.js.

You can get the complete source code for the CRUD express app from this repo https://github.com/Chimise/express-typescript.git.

Getting Started

We will start by creating a new directory for this project and initializing a new package.json file inside this directory.

mkdir express-typescript
cd express-typescript
npm init --yes

The --yes flag initializes the package.json file using the default configuration. Create a src directory inside this current directory where all our source codes will be placed and within the src directory create an index.js file.

mkdir -p src/index.js

We will now install the express library and create a simple express application.

npm install express

Inside src/index.js file, add the following code:

const express = require('express');
const app = express();
const port = process.env.PORT || 3500;
app.use(express.json());
app.get('/', (req, res) => {
    res.send('<h1>Welcome to Book Api<h1>');
})
app.listen(port, () => {
    console.log('Server running on http://localhost:%i',port)
});

Start the server by running node src/index.js and visit http://localhost:3500 to see the text Welcome to Book Api.

Installing TypeScript

We will get started by installing Typescript as a development dependency because we will be using it only during development, the Node.js environment cannot run Typescript code but can execute the Javascript code emitted by the typescript compiler. We will also need to tell the typescript compiler how the Node.js built-in modules and the express library are implemented by adding types to them, doing this however will be very tedious.

Thankfully, a lot of people have contributed to creating Typescript declaration (.d.ts) files for different Javascript libraries found in the DefinitelyTyped Repository. For Javascript libraries without a Typescript declaration file, You can check whether the typings for such libraries exist in DefinitelyTyped Repository by running npm install -D @types/<packagename>.

Install the Typescript compiler and the typings for node.js and express by running:

npm install -D typescript @types/node @types/express-typescript

After the installation is completed, we will need to generate a tsconfig.json file at the root project directory to customize the default behavior of the Typescript compiler. To generate the tsconfig.json, we will run the tsc binary using npx command in the root project directory.

npx tsc --init

At the time of writing this, the generated tsconfig.json contains the below configuration enabled by default:

{
    "compilerOptions": {
        "target": "es2016",
        "module": "commonjs",
        "rootDir": "./",
        "outDir": "./",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true
    }
}

The default options for the compilerOptions property of the tsconfig.json file are explained below:

  • target: Specifies the Javascript version of the code that will be emitted by the compiler (es2016, es2022).

  • module: Indicates the module system to use in the emitted code, commonjs is the default for Node.js and uses the require keyword.

  • rootDir: Specifies the root directory for our project.

  • outDir: Allows us to specify the output directory of the trans-compiled Javascript code.

  • esModuleInterop: If this option is specified, we can import commonjs modules into ES6 module codebase.

  • forceConsistentCasingInFileNames: Enforces using the exact casing when importing a file.

  • strict: If true, enables all strict type-checking options.

  • skipLibCheck: Skips type-checking all .d.ts files in your project.

Feel free to explore other configuration options in the Typescript documentation.

We will update the outDir option which is the root directory by default to dist directory, we will also be adding some new options. Update the tsconfig.json file with the content below.

{
    "compilerOptions": {
        "outDir": "./dist",

        // Other options remain unchaanged
    },
    "include": ["./src/**/*"],
      "exclude": ["node_modules"]
}

We also explicitly instructed the compiler to only compile files within the src directory and exclude files in the node_modules directory.

Rewriting the Express app to Typescript

We will start by renaming the index.js file to index.ts and then replacing commonjs require syntax with ES6 module import syntax. The .ts extension indicates that this is a Typescript file, after renaming the file, replace the existing code with the code below.

// src/index.ts
import express, {Request, Response} from 'express';

const app = express();

const port = process.env.PORT || 3500;

app.use(express.json());

app.get('/', (req: Request, res: Response) => {
    res.send('<h1>Welcome to Book Api<h1>');
})

app.listen(port, () => {
    console.log('Server running on http://localhost:%i',port)
});

We added the Request and Response types that come from @types/express package to req and res parameters respectively.

Compiling and Running the Express Typescript App

During development, we want to be able to compile our Typescript source code to Javascript whenever our source code changes and also restart the server automatically to update the changes. To do this, we must install two npm packages, Nodemon and Concurrently. Nodemon helps us restart the server when a file content changes while Concurrently helps us execute multiple commands in parallel and works across different OS.


npm install -D concurrently nodemon

After installing the dev dependencies, we will add new scripts to the scripts field in package.json file as shown below:

{
    "scripts": {
    "watch_ts": "tsc -w",
    "watch_node": "nodemon ./dist/src/index.js",
    "dev": "concurrently -k \"npm run watch_ts\" \"sleep 5 && npm run watch_node\"",
    "build": "tsc",
    "start": "node ./dist/src/index.js"
    }
}

The scripts in the scripts field in package.json file are explained below:

  • watch-ts: The -w flag starts the Typescript compiler tsc in watch mode, compiles the typescript files on startup, and recompiles when any of the typescript files change.

  • watch_node: Watches for changes in the project directory and restarts the node.js server.

  • dev: Runs the two scripts above simultaneously, the -k flag stops other scripts if one script fails. Because the scripts run simultaneously, the watch_node command fails if no dist directory exists, this will happen if the dev script is run for the first time and the typescript compiler is yet to compile the typescript files and create a dist directory. To prevent this, we first run the command sleep 5 to wait for five seconds before running the watch_node script.

  • build: Compiles the typescript file to javascript and exits on completion.

  • start: Starts the Node.js server

Now, go back to your terminal and run npm run dev to start the development server and visit http://localhost:3500 to see the response.

Building a CRUD Express App with Typescript.

We will create a simple express application for storing books, each book will have a unique id, a title, an author, and a timestamp to track when the book was stored. The application will also be able to retrieve, update or remove the stored book. Start by creating three directories controllers, routes and services inside the src directory.


mkdir controllers routes services

Creating a Book type

Create a types.ts file inside the src directory where all our Typescript types will be kept. Open the newly created types.ts file and add the code below.

// src/types.ts
export interface Book {
    title: string;
    author: string;
    createdAt: Date;
    id: number;
}

We are using the interface keyword to create a Typescript type Book with the above properties, we exporting this type to use it in other files. Note that this type will be stripped off from the transpiled Javascript code.

Adding Logic for Storing Books

We will create a BookService class with methods for storing, retrieving, updating, and deleting a Book object. Create a file book.ts inside the services directory and add the code below.

import type {Book} from '../types';

class BookService {
    // instance property that stores object of type Book in an array
    private _books: Book[] = [];
    // A static property id to keep track of the id assigned to the Book object
    static id: number = 1;
    // Accepts an object, creates a Book object and adds it to the _books array
    add(data: Omit<Book, 'id'|'createdAt'>) {;
        const book: Book = {
            ...data,
            id: BookService.id++,
            createdAt: new Date()
        };
        this._books.push(book);

        return book;
    }

    // Remove a book from the _book array field with the same id as the input id
    remove(id: number) {
        const index = this._books.findIndex(book => book.id === id);
        if(index === -1) {
            return null;
        }
        // Remove the book from the _book array
        const book = this._books.splice(index, 1)[0];
        return book;
    }

    // Finds a book in the _book array field using the id
    find(id: number) {
        const book = this._books.find(book => book.id === id);
        if(!book) {
            return null;
        }
        return book;
    }
    // Update a book in the _books array with an id matching the input id
    update(id:number, data: Partial<Omit<Book, 'id'| 'createdAt'>>) {
        const index = this._books.findIndex(book => book.id === id);
        if(index === -1) {
            return null;
        }
        // Filter out properties with value undefined
        data = Object.fromEntries(Object.entries(data).filter(([_, value]) => !!value));
        const book: Book = {
            ...this._books[index],
            ...data
        }
        this._books[index] = book;
        return book;
    }

    // A getter function that returns the _books array field
    get books () {
        return this._books;
    }

}

export default new BookService();

The BookService Class has a _books array property for storing objects of type Book, the _books property has a private modifier which means that this property should not be accessed outside the class BookService, doing so will generate a compiler error.

The id static property is used to create a unique id for the Book objects. we are using Typescript utility types Omit for removing all listed fields from a type and Partial to make all the fields in a type optional.

Creating a Book Controller

Inside the controllers directory, create a new file book.ts, the file will contain code for handling requests and sending responses. Add the code below to the created book.ts file.


import type {Request, Response} from 'express';
import BookService from '../services/book';

export const getBook = (req: Request, res: Response ) => {
    const id = parseInt(req.params.id as string);
    const book = BookService.find(id);
    if(!book) {
        return res.status(404).json({message: 'Book not found'});
    }
    res.json(book);

}

export const createBook = (req: Request, res: Response) => {
    const {title, author} = req.body;
    if(!title) {
        return res.status(400).json({message: 'Book title is required'});
    }
    if(!author) {
        return res.status(400).json({message: 'Book author is required'});
    }
    const book = BookService.add({title, author});
    res.status(201).json(book);
}

export const updateBook = (req: Request, res: Response) => {
    const id = parseInt(req.params.id as string);
    const {title, author} = req.body;
    if(!title && !author) {
        return res.status(400).json({message: 'Book title or author is required'});
    }
    const book = BookService.update(id, {title, author});
    if(!book) {
        return res.status(404).json({message: 'Book not found'});
    }
    res.json(book);
}

export const deleteBook = (req: Request, res: Response) => {
    const id = parseInt(req.params.id as string);
    const book = BookService.remove(id);
    if(!book) {
        return res.status(404).send({message: 'Book not found'});
    }
    res.json(book);
}

export const allBooks = (req: Request, res: Response) => {
    const books = BookService.books;
    res.json(books);
}

In the code above, we are handling validation on user inputs and using the BookService methods to perform the desired operation. After this operation, we will return either a success or an error response.

Creating a Book Routes

Inside the routes directory, create a new file book.ts that will map routes to the already created controllers. Add the code below to the content of the file.

import {Router} from 'express';
import {getBook, createBook, updateBook, deleteBook, allBooks} from '../controllers/book';

const router = Router();
// matches method GET and route /books/1
router.get('/:id', getBook);
// matches method GET and route /books
router.get('/', allBooks);
// matches method POST and route /books
router.post('/', createBook);
// matches method PUT and route /books/1
router.put('/:id', updateBook);
// matches method DELETE and route /books/1
router.delete('/:id', deleteBook);

export default router;

We imported the controller functions created previously and mapped them to their respective routes.

Adding the Book Routes to the Express App

To add the created book routes to our express application, open the index.ts file inside the src directory and replace the previous code with the code below.

import express, {Request, Response} from 'express';
import bookRouter from './routes/book';

const app = express();
const port = process.env.PORT || 3500;

app.use(express.json());
app.get('/', (req: Request, res: Response) => {
    res.send('<h1>Welcome to Book Api<h1>');
});
app.use('/books', bookRouter);
app.listen(port, () => {
    console.log('Server running on http://localhost:%i',port)
});

Save all the files and restart the server if not already running. To test the book API routes, we will use curl utility command from the terminal. We will start by adding a book to our application.

curl -X POST \
  -H 'Content-Type: application/json' \
  -d '{"title": "Lorem Ipsum", "author": "John Doe"}' \
  http://localhost:3500/books

The response on your terminal will look similar to this, the createdAt field will be different:

{"title":"Lorem Ipsum","author":"John Doe","id":1,"createdAt":"2023-03-20T16:39:31.967Z"}

We can update the title of this book to Dark Night:

curl -X PUT \
    -H 'Content-Type: application/json' \
    -d '{"title": "Dark Night"}' \
    http://localhost:3500/books/1

The response on your terminal should be similar to this:

{"title":"Dark Night","author":"John Doe","id":1,"createdAt":"2023-03-20T16:39:31.967Z"}

We will now remove this book from the _book array:

curl -X DELETE http://localhost:3500/books/1

You should get a response similar to this on your terminal:

{"title":"Dark Night","author":"John Doe","id":1,"createdAt":"2023-03-20T16:39:31.967Z"}

To verify that the _book array is now empty, we can get all the books:

curl http://localhost:3500/books

The response should be an empty array:

[]

Our Book CRUD API is now fully completed and working as expected, we learned how to set up an express application with Typescript and how to add custom types and use utility types in our Typescript project.