HOW TO BUILD A RESTFUL API USING TYPESCRIPT

Introduction

TypeScript, a statically typed superset of JavaScript, has gained immense popularity for its ability to enhance code quality and developer productivity. When combined with Node.js and frameworks like Express, TypeScript becomes a powerful tool for building robust and scalable server-side applications. In this guide, we will develop a RESTful API for managing a collection of books. This complex TypeScript program will demonstrate advanced TypeScript features, including interfaces, classes, generics, asynchronous operations, and middleware integration. By the end of this tutorial, you’ll have a solid understanding of how to create a type-safe and maintainable API using TypeScript.

Prerequisites

Before diving into the code, ensure you have the following installed:

Project Setup

Step 1: Initialize the Project

First, create a new directory for your project and initialize it with npm:

mkdir typescript-rest-api
cd typescript-rest-api
npm init -y
Bash

Step 2: Install Dependencies

Install the necessary dependencies, including TypeScript, Express, and their type definitions:

npm install express
npm install --save-dev typescript ts-node @types/node @types/express nodemon
Bash

express: A minimal and flexible Node.js web application framework.typescript: Adds TypeScript support.ts-node: Executes TypeScript files directly.@types/node & @types/express: Provides TypeScript definitions for Node.js and Express.nodemon: Automatically restarts the server on code changes.

Step 3: Configure TypeScript

Initialize a TypeScript configuration file:

npx tsc --init
Bash

Modify the tsconfig.json to suit our project needs. Ensure the following settings are adjusted:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  }
}
Bash

Step 4: Set Up Scripts

Update the package.json to include scripts for building and running the project:

"scripts": {
  "start": "node dist/index.js",
  "dev": "nodemon src/index.ts",
  "build": "tsc"
},
Bash

Developing the RESTful API

Step 1: Project Structure

Create the following directory structure

typescript-rest-api/

├── src/
   ├── controllers/
      └── bookController.ts
   ├── models/
      └── book.ts
   ├── routes/
      └── bookRoutes.ts
   ├── services/
      └── bookService.ts
   └── index.ts

├── tsconfig.json
└── package.json
Bash

Step 2: Define the Book Model

Create a Book interface and a class to manage book data.

File: src/models/book.ts

export interface Book {
    id: number;
    title: string;
    author: string;
    publishedDate: Date;
    genre: string;
}

export class BookStore {
    private books: Book[] = [];
    private currentId: number = 1;

    getAll(): Book[] {
        return this.books;
    }

    getById(id: number): Book | undefined {
        return this.books.find(book => book.id === id);
    }

    create(book: Omit<Book, 'id'>): Book {
        const newBook: Book = { id: this.currentId++, ...book };
        this.books.push(newBook);
        return newBook;
    }

    update(id: number, updatedBook: Partial<Omit<Book, 'id'>>): Book | undefined {
        const book = this.getById(id);
        if (book) {
            Object.assign(book, updatedBook);
            return book;
        }
        return undefined;
    }

    delete(id: number): boolean {
        const index = this.books.findIndex(book => book.id === id);
        if (index !== -1) {
            this.books.splice(index, 1);
            return true;
        }
        return false;
    }
}
Bash

Explanation:

Omit<Book, 'id'>: Utilizes TypeScript’s utility types to exclude the id property when creating a new book.

Book Interface: Defines the structure of a book object.

BookStore Class: Manages the in-memory collection of books, providing methods to perform CRUD (Create, Read, Update, Delete) operations.

Step 3: Create the Book Service

The service layer interacts with the data model to perform operations.

import { Book, BookStore } from '../models/book';

export class BookService {
    private store: BookStore;

    constructor(store: BookStore) {
        this.store = store;
    }

    getAllBooks(): Book[] {
        return this.store.getAll();
    }

    getBookById(id: number): Book | undefined {
        return this.store.getById(id);
    }

    addBook(bookData: Omit<Book, 'id'>): Book {
        return this.store.create(bookData);
    }

    updateBook(id: number, bookData: Partial<Omit<Book, 'id'>>): Book | undefined {
        return this.store.update(id, bookData);
    }

    removeBook(id: number): boolean {
        return this.store.delete(id);
    }
}
Bash

Explanation:

Methods: Provides methods to get all books, get a book by ID, add a new book, update an existing book, and remove a book.

BookService Class: Acts as an intermediary between the controller and the data model, encapsulating business logic.

tep 4: Develop the Book Controller

The controller handles HTTP requests and interacts with the service layer.

File: src/controllers/bookController.ts

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

export class BookController {
    private service: BookService;

    constructor(service: BookService) {
        this.service = service;
    }

    getAllBooks = (req: Request, res: Response): void => {
        const books = this.service.getAllBooks();
        res.json(books);
    };

    getBookById = (req: Request, res: Response): void => {
        const id = parseInt(req.params.id, 10);
        const book = this.service.getBookById(id);
        if (book) {
            res.json(book);
        } else {
            res.status(404).send({ message: 'Book not found' });
        }
    };

    addBook = (req: Request, res: Response): void => {
        const newBook = this.service.addBook(req.body);
        res.status(201).json(newBook);
    };

    updateBook = (req: Request, res: Response): void => {
        const id = parseInt(req.params.id, 10);
        const updatedBook = this.service.updateBook(id, req.body);
        if (updatedBook) {
            res.json(updatedBook);
        } else {
            res.status(404).send({ message: 'Book not found' });
        }
    };

    deleteBook = (req: Request, res: Response): void => {
        const id = parseInt(req.params.id, 10);
        const success = this.service.removeBook(id);
        if (success) {
            res.status(204).send();
        } else {
            res.status(404).send({ message: 'Book not found' });
        }
    };
}
Bash

Explanation:

deleteBook: Deletes a book by ID.

BookController Class: Defines methods to handle HTTP requests for various endpoints.

Endpoints Handled:

getAllBooks: Retrieves all books.

getBookById: Retrieves a single book by its ID.

addBook: Adds a new book.

updateBook: Updates an existing book.

Step 5: Define the Book Routes

Set up the routing for the API endpoints.

File: src/routes/bookRoutes.ts

import { Router } from 'express';
import { BookController } from '../controllers/bookController';
import { BookService } from '../services/bookService';
import { BookStore } from '../models/book';

const router = Router();
const store = new BookStore();
const service = new BookService(store);
const controller = new BookController(service);

// Initialize with some sample data
store.create({ title: '1984', author: 'George Orwell', publishedDate: new Date('1949-06-08'), genre: 'Dystopian' });
store.create({ title: 'To Kill a Mockingbird', author: 'Harper Lee', publishedDate: new Date('1960-07-11'), genre: 'Fiction' });

// Define routes
router.get('/books', controller.getAllBooks);
router.get('/books/:id', controller.getBookById);
router.post('/books', controller.addBook);
router.put('/books/:id', controller.updateBook);
router.delete('/books/:id', controller.deleteBook);

export default router;
Bash

Explanation:

DELETE /books/:id: Delete a book by ID.

Router Setup: Uses Express’s Router to define API endpoints.

Dependency Injection: Initializes BookStore, BookService, and BookController to ensure a clear separation of concerns.

Sample Data: Adds initial books to the store for testing purposes.

Endpoints Defined:

GET /books: Retrieve all books.

GET /books/:id: Retrieve a book by ID.

POST /books: Add a new book.

PUT /books/:id: Update a book by ID.

Step 6: Create the Server Entry Point

Set up the Express server to use the defined routes and middleware.

File: src/index.ts

import express, { Application, Request, Response, NextFunction } from 'express';
import bookRoutes from './routes/bookRoutes';

const app: Application = express();
const PORT = process.env.PORT || 3000;

// Middleware to parse JSON
app.use(express.json());

// Simple logging middleware
app.use((req: Request, res: Response, next: NextFunction) => {
    console.log(`${req.method} ${req.path}`);
    next();
});

// Use book routes
app.use('/api', bookRoutes);

// Error handling middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
    console.error(err.stack);
    res.status(500).send({ message: 'Something went wrong!' });
});

// Start the server
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});
Bash

Explanation:

Server Startup: Starts the server on the specified port and logs a confirmation message

Express App Initialization: Creates an instance of an Express application.

Middleware:

express.json(): Parses incoming JSON requests.

Logging Middleware: Logs each incoming request’s method and path.

Error Handling Middleware: Catches and handles errors gracefully.

Route Integration: Mounts the book routes under the /api path.

Step 7: Running the Application

Use the development script to run the server with automatic restarts on code changes:

npm run dev
Bash

Output:

[nodemon] 2.0.15
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src/**/* 
[nodemon] watching extensions: ts 
[nodemon] starting `ts-node src/index.ts`
Server is running on port 3000
Bash

Step 8: Testing the API

You can use tools like Postman or cURL to interact with the API.

1. Get All Books

Request:

GET http://localhost:3000/api/books
Bash

Response:

[
    {
        "id": 1,
        "title": "1984",
        "author": "George Orwell",
        "publishedDate": "1949-06-08T00:00:00.000Z",
        "genre": "Dystopian"
    },
    {
        "id": 2,
        "title": "To Kill a Mockingbird",
        "author": "Harper Lee",
        "publishedDate": "1960-07-11T00:00:00.000Z",
        "genre": "Fiction"
    }
]
Bash

2. Get a Book by ID

Request:

GET http://localhost:3000/api/books/1
Bash

Response:

{
    "id": 1,
    "title": "1984",
    "author": "George Orwell",
    "publishedDate": "1949-06-08T00:00:00.000Z",
    "genre": "Dystopian"
}
Bash

3. Add a New Book

Request:

POST http://localhost:3000/api/books
Content-Type: application/json

{
    "title": "The Great Gatsby",
    "author": "F. Scott Fitzgerald",
    "publishedDate": "1925-04-10",
    "genre": "Tragedy"
}
Bash

Response:

{
    "id": 3,
    "title": "The Great Gatsby",
    "author": "F. Scott Fitzgerald",
    "publishedDate": "1925-04-10T00:00:00.000Z",
    "genre": "Tragedy"
}
Bash

4. Update a Book

Request:

PUT http://localhost:3000/api/books/3
Content-Type: application/json

{
    "genre": "Classic"
}
Bash

Response:

{
    "id": 3,
    "title": "The Great Gatsby",
    "author": "F. Scott Fitzgerald",
    "publishedDate": "1925-04-10T00:00:00.000Z",
    "genre": "Classic"
}
Bash

5. Delete a Book

Request:

DELETE http://localhost:3000/api/books/3
Bash

Response:

Status: 204 No Content
Bash

Conclusion

In this guide, we built a RESTful API using TypeScript, Express, and Node.js. We covered advanced TypeScript features such as interfaces, classes, generics, and middleware integration to create a type-safe and maintainable application. By following a layered architecture with models, services, controllers, and routes, we ensured a clear separation of concerns, enhancing the scalability and readability of the codebase.

This project serves as a foundation for more complex applications, allowing you to extend functionalities, integrate databases, implement authentication, and more. Leveraging TypeScript’s strong typing and modern JavaScript features, you can build robust APIs that are easier to debug and maintain.

For further learning and exploration, consider the following resources:

References

  1. TypeScript Handbook
  2. Express.js Official Website
  3. Node.js Official Documentation
  4. Nodemon Documentation
  5. TypeScript Utility Types
  6. REST API Design Guide
  7. Postman API Testing Tool
  8. cURL Official Website

By adhering to best practices and utilizing TypeScript’s powerful features, you can develop secure, efficient, and scalable APIs tailored to your application’s needs.

Resize text
Scroll to Top