Last updated: February 5, 2025

<span aria-hidden="true" id="building-restful-apis-with-nodejs-express-and-typescript"></span>

# Building restful APIs with Node.js, Express, and TypeScript

![Tim Koschützki](/assets/images/teammates/avatar-tim-kos-1.jpg?dpl=dpl_3c6w24doFHQ83A1AtAy73NTtMxiB)

#### Tim Koschützki

Co-founder · Berlin, Germany · Show bio

[](https://x.com/tim%5Fkos)[](https://github.com/tim-kos)

Developing robust and maintainable RESTful APIs is a critical skill in modern web development. In this post, we'll explore how to build a RESTful API using Node.js, Express, and TypeScript, covering everything from initial setup to deploying your application.

<span aria-hidden="true" id="introduction"></span>

## Introduction

Building a full-stack API allows you to create powerful back-end services that interact seamlessly with front-end applications. Node.js and Express are popular choices for back-end development due to their performance and scalability. Incorporating TypeScript adds static typing and advanced tooling, resulting in more maintainable and error-resistant code.

<span aria-hidden="true" id="system-requirements"></span>

## System requirements

Before you begin, ensure your system meets these requirements:

* Node.js 18.0.0 or later (recommended for Express 5.x)
* npm 8.0.0 or later
* TypeScript 4.5 or later

<span aria-hidden="true" id="setting-up-the-environment"></span>

## Setting up the environment

Start by verifying your Node.js and npm versions:

```bash
node -v
npm -v

```

If Node.js isn’t installed, download it from the [official Node.js website⁠](https://nodejs.org/en/).

Create a new project directory and initialize it:

```bash
mkdir stack-api
cd stack-api
npm init -y

```

Install the necessary dependencies:

```bash
npm install express helmet cors dotenv
npm install typeorm pg reflect-metadata
npm install --save-dev typescript ts-node @types/node @types/express nodemon

```

Initialize a TypeScript configuration file:

```bash
npx tsc --init

```

Replace the contents of your generated `tsconfig.json` with the following configuration, which is recommended for modern Node.js projects:

```json
{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

```

Create a `.env` file for environment variables:

```plaintext
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_USER=your_user
DB_PASS=your_password
DB_NAME=your_database

```

<span aria-hidden="true" id="project-structure"></span>

## Project structure

Organize your project using the following structure:

```
src/
├── config/
│   └── database.ts
├── controllers/
├── middleware/
├── models/
├── routes/
└── index.ts

```

<span aria-hidden="true" id="setting-up-the-database-connection"></span>

## Setting up the database connection

Create the file `src/config/database.ts`:

```typescript
import { DataSource } from 'typeorm'
import { User } from '../models/User'
import dotenv from 'dotenv'

dotenv.config()

export const AppDataSource = new DataSource({
  type: 'postgres',
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || '5432'),
  username: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  synchronize: process.env.NODE_ENV === 'development', // Disable in production
  logging: process.env.NODE_ENV === 'development',
  entities: [User],
  subscribers: [],
  migrations: [],
})

```

<span aria-hidden="true" id="creating-the-user-model"></span>

## Creating the user model

Create `src/models/User.ts`:

```typescript
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id!: number

  @Column()
  name!: string

  @Column({ unique: true })
  email!: string

  @Column()
  password!: string

  @CreateDateColumn()
  createdAt!: Date

  @UpdateDateColumn()
  updatedAt!: Date
}

```

<span aria-hidden="true" id="error-handling-middleware"></span>

## Error handling middleware

Create `src/middleware/errorHandler.ts`:

```typescript
import { Request, Response, NextFunction } from 'express'

export class AppError extends Error {
  statusCode: number
  status: string

  constructor(message: string, statusCode: number) {
    super(message)
    this.statusCode = statusCode
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'
  }
}

export const errorHandler = (
  err: Error | AppError,
  req: Request,
  res: Response,
  next: NextFunction,
) => {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      status: err.status,
      message: err.message,
    })
  }

  console.error('Error:', err.stack || err)
  return res.status(500).json({
    status: 'error',
    message: process.env.NODE_ENV === 'development' ? err.message : 'Internal server error',
  })
}

```

<span aria-hidden="true" id="setting-up-the-express-server"></span>

## Setting up the Express Server

Create `src/index.ts`:

```typescript
import 'reflect-metadata'
import express from 'express'
import helmet from 'helmet'
import cors from 'cors'
import dotenv from 'dotenv'
import { AppDataSource } from './config/database'
import { errorHandler } from './middleware/errorHandler'
import userRoutes from './routes/userRoutes'

dotenv.config()

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

// Security middleware
app.use(helmet())
app.use(cors())
app.use(express.json({ limit: '10kb' }))

// Routes
app.use('/api/v1/users', userRoutes)

// Global error handling
app.use(errorHandler)

// Database connection and server startup
AppDataSource.initialize()
  .then(() => {
    console.log('Database connected successfully.')
    app.listen(port, () => {
      console.log(`Server running on port ${port} in ${process.env.NODE_ENV} mode`)
    })
  })
  .catch((error) => {
    console.error('Error connecting to database:', error)
    process.exit(1)
  })

```

<span aria-hidden="true" id="user-controller"></span>

## User controller

Create `src/controllers/userController.ts`:

```typescript
import { Request, Response, NextFunction } from 'express'
import { AppDataSource } from '../config/database'
import { User } from '../models/User'
import { AppError } from '../middleware/errorHandler'

const userRepository = AppDataSource.getRepository(User)

export const getUsers = async (req: Request, res: Response, next: NextFunction) => {
  try {
    const users = await userRepository.find({
      select: ['id', 'name', 'email', 'createdAt'],
    })
    res.json({ status: 'success', data: users })
  } catch (err) {
    next(err)
  }
}

export const getUser = async (req: Request, res: Response, next: NextFunction) => {
  try {
    const user = await userRepository.findOne({
      where: { id: parseInt(req.params.id) },
      select: ['id', 'name', 'email', 'createdAt'],
    })
    if (!user) {
      throw new AppError('User not found', 404)
    }
    res.json({ status: 'success', data: user })
  } catch (err) {
    next(err)
  }
}

export const createUser = async (req: Request, res: Response, next: NextFunction) => {
  try {
    const { name, email, password } = req.body
    if (!name || !email || !password) {
      throw new AppError('Please provide name, email and password', 400)
    }

    const user = userRepository.create({ name, email, password })
    await userRepository.save(user)

    res.status(201).json({
      status: 'success',
      data: {
        id: user.id,
        name: user.name,
        email: user.email,
      },
    })
  } catch (err) {
    next(err)
  }
}

export const updateUser = async (req: Request, res: Response, next: NextFunction) => {
  try {
    const user = await userRepository.findOne({
      where: { id: parseInt(req.params.id) },
    })

    if (!user) {
      throw new AppError('User not found', 404)
    }

    userRepository.merge(user, req.body)
    const updatedUser = await userRepository.save(user)

    res.json({
      status: 'success',
      data: {
        id: updatedUser.id,
        name: updatedUser.name,
        email: updatedUser.email,
      },
    })
  } catch (err) {
    next(err)
  }
}

export const deleteUser = async (req: Request, res: Response, next: NextFunction) => {
  try {
    const result = await userRepository.delete(req.params.id)

    if (result.affected === 0) {
      throw new AppError('User not found', 404)
    }

    res.status(204).json({
      status: 'success',
      data: null,
    })
  } catch (err) {
    next(err)
  }
}

```

<span aria-hidden="true" id="user-routes"></span>

## User routes

Create `src/routes/userRoutes.ts`:

```typescript
import { Router } from 'express'
import {
  getUsers,
  getUser,
  createUser,
  updateUser,
  deleteUser,
} from '../controllers/userController'

const router = Router()

router.route('/').get(getUsers).post(createUser)
router.route('/:id').get(getUser).patch(updateUser).delete(deleteUser)

export default router

```

<span aria-hidden="true" id="running-the-application"></span>

## Running the application

Add these scripts to your `package.json`:

```json
{
  "scripts": {
    "start": "node dist/index.js",
    "dev": "nodemon src/index.ts",
    "build": "tsc",
    "lint": "eslint . --ext .ts",
    "test": "jest"
  }
}

```

Start the development server with the following command:

```bash
npm run dev

```

Your API will be available at `http://localhost:3000/api/v1/users`.

<span aria-hidden="true" id="testing-the-api"></span>

## Testing the API

You can test the endpoints using tools like [Postman⁠](https://www.postman.com/) or cURL:

```bash
# Get all users
curl -fsSL http://localhost:3000/api/v1/users

# Create a user
curl -fsSL -X POST http://localhost:3000/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{"name":"John Doe","email":"john@example.com","password":"secret123"}'

# Get a specific user
curl -fsSL http://localhost:3000/api/v1/users/1

# Update a user
curl -fsSL -X PATCH http://localhost:3000/api/v1/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"John Smith"}'

# Delete a user
curl -fsSL -X DELETE http://localhost:3000/api/v1/users/1

```

<span aria-hidden="true" id="api-documentation"></span>

## API documentation

For better maintainability, consider integrating Swagger/OpenAPI into your project. You can use packages such as `swagger-jsdoc` and `swagger-ui-express` to automatically generate API documentation from your routes. Refer to the[Swagger documentation⁠](https://swagger.io/tools/open-source/swagger-ui/) for more details.

<span aria-hidden="true" id="deployment-considerations"></span>

## Deployment considerations

When preparing your API for production, keep in mind the following best practices:

* Disable automatic schema synchronization by setting `synchronize` to false in your TypeORM configuration.
* Configure secure HTTP headers and rate limiting.
* Use environment variables to manage sensitive configurations and credentials.
* Thoroughly test your API using unit and integration tests.

Consult the[Express.js deployment guide⁠](https://expressjs.com/en/advanced/best-practice-performance.html) and[TypeORM documentation⁠](https://typeorm.io/) for further advice.

<span aria-hidden="true" id="conclusion"></span>

## Conclusion

This tutorial has provided you with a comprehensive guide to building a secure and maintainable RESTful API using Node.js, Express, and TypeScript. We covered environment setup, database integration with TypeORM, detailed error handling, and essential security middleware. Additionally, we discussed documentation and deployment strategies to help you build production-ready applications.

If you require advanced file upload management capabilities, consider exploring Transloadit's robust upload API ([https://transloadit.com](/index.md)).

\#nodejs#expressjs#typescript#restful-api#backend-development

### 👩‍💻 Join 20k+ developers

Sign up for our [monthly newsletter](/newsletters.md) to receive direct links to 3 exclusive tech — and 2 product updates. No less, no more.

Your email:

Get access

## File uploading and encoding. Made simple.

Transloadit streamlines file handling for developers, trusted by brands like Coursera and The New York Times. We’re known for a reliable API, top-notch support, and a strong commitment to open source, with projects like [Uppy⁠](https://uppy.io) and [Tus⁠](https://tus.io) setting standards in file processing.

[Sign up](/c/)[Book a Demo](https://survey.typeform.com/to/kRg47Xi5)

No credit card needed · 5 GB included in the free plan

Cancel anytime
