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 therequire
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 compilertsc
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, thewatch_node
command fails if nodist
directory exists, this will happen if thedev
script is run for the first time and the typescript compiler is yet to compile the typescript files and create adist
directory. To prevent this, we first run the commandsleep 5
to wait for five seconds before running thewatch_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.