Skip to content

Express.js TypeScript APIs — Complete Backend Guide

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Express.js TypeScript APIs. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript transforms Express.js development by adding type safety to request handlers, middleware, and route parameters, catching errors at compile time before they reach production.

What You'll Learn

  • Typed Express.js routes and handlers
  • Request validation with Zod
  • Middleware typing patterns
  • Error handling middleware
  • Production-ready API structure

Why It Matters

Express.js without TypeScript relies on manual inspection for request bodies, query params, and response shapes. A single mistyped property can crash your API. TypeScript catches these issues during development, reducing runtime errors by up to 40% in backend applications.

Real-World Use

The Durga Antivirus Pro license verification API handles millions of requests daily using Express.js with TypeScript. Typed request handlers ensure that license keys, device IDs, and response payloads are always in the correct format — no surprises in production.

Learning Path

flowchart LR
  A[Node Setup] --> B[Express APIs]
  B --> C[Next.js]
  B --> D[NestJS]
  B --> E[You Are Here]
  C --> F[Database Access]

Setting Up an Express Project

Create a new project and install dependencies:

mkdir my-api && cd my-api
npm init -y
npm install express
npm install --save-dev typescript @types/express @types/node tsx

The @types/express package provides type definitions for Express's Request, Response, and NextFunction objects.

Basic Typed Server

import express, { Request, Response } from 'express';

const app = express();
const port = 3000;

app.get('/health', (_req: Request, res: Response) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

Expected output:

{ "status": "ok", "timestamp": "2026-06-28T12:00:00.000Z" }

Typed Request Handlers

Express's Request type is generic — you can specify body, params, and query types:

import { Request, Response } from 'express';

interface CreateUserBody {
  name: string;
  email: string;
  age?: number;
}

interface UserParams {
  id: string;
}

app.post('/users', (req: Request<{}, {}, CreateUserBody>, res: Response) => {
  const { name, email } = req.body;
  // req.body is typed as CreateUserBody
  console.log(`Creating user: ${name} (${email})`);
  res.status(201).json({ id: '123', name, email });
});

app.get('/users/:id', (req: Request<UserParams>, res: Response) => {
  // req.params.id is typed as string
  res.json({ id: req.params.id, name: 'John Doe' });
});

Why this matters: Without generics, req.body is any. A typo like req.body.emial compiles fine and crashes at runtime. With typed handlers, the compiler catches it immediately.

Request Validation with Zod

Raw type assertions don't validate at runtime — you need a validation library. Zod integrates perfectly with TypeScript:

npm install zod
import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});

type CreateUserInput = z.infer<typeof CreateUserSchema>;

app.post('/users', (req: Request, res: Response) => {
  const result = CreateUserSchema.safeParse(req.body);

  if (!result.success) {
    return res.status(400).json({
      error: 'Validation failed',
      details: result.error.flatten(),
    });
  }

  // result.data is typed as CreateUserInput
  const user: CreateUserInput = result.data;
  res.status(201).json({ id: '456', ...user });
});

Expected output (invalid input):

{
  "error": "Validation failed",
  "details": {
    "fieldErrors": {
      "email": ["Invalid email"]
    }
  }
}

The z.infer<typeof Schema> pattern gives you both runtime validation and compile-time type safety from a single source of truth.

Typed Middleware

Middleware functions in Express receive Request, Response, and NextFunction. Create typed middleware for authentication, logging, and error handling:

import { Request, Response, NextFunction } from 'express';

// Extend the Request type to add custom properties
declare global {
  namespace Express {
    interface Request {
      userId?: string;
    }
  }
}

function authMiddleware(req: Request, res: Response, next: NextFunction): void {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    res.status(401).json({ error: 'Unauthorized' });
    return;
  }

  // Verify token (simplified)
  req.userId = 'user_123';
  next();
}

// Now use it in routes
app.get('/profile', authMiddleware, (req: Request, res: Response) => {
  res.json({ userId: req.userId, name: 'Protected User' });
});

Global augmentation is the standard pattern for adding custom properties to Express's Request. The declare global { namespace Express { interface Request { ... } } } block merges your property into the existing type.

Error Handling Middleware

Express error handlers have four parameters — TypeScript helps you get the signature right:

import { Request, Response, NextFunction } from 'express';

class AppError extends Error {
  constructor(
    public statusCode: number,
    public message: string,
    public isOperational = true
  ) {
    super(message);
    this.name = 'AppError';
  }
}

function errorHandler(
  err: Error,
  _req: Request,
  res: Response,
  _next: NextFunction
): void {
  if (err instanceof AppError) {
    res.status(err.statusCode).json({
      error: err.message,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
    });
  } else {
    console.error('Unhandled error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
}

// Register error handler LAST
app.use(errorHandler);

// Usage in routes
app.get('/fail', (_req: Request, _res: Response) => {
  throw new AppError(400, 'Bad request');
});

The four-parameter signature (err, req, res, next) is what tells Express this is an error handler. TypeScript ensures you don't accidentally omit a parameter.

Production API Structure

For real applications, organize your code into modules:

src/
  routes/
    users.ts
    auth.ts
  middleware/
    auth.ts
    validate.ts
  services/
    user-service.ts
  types/
    index.ts
  app.ts
  server.ts
// src/types/index.ts
export interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

export interface CreateUserRequest {
  name: string;
  email: string;
}

// src/services/user-service.ts
import { User, CreateUserRequest } from '../types';

const users: User[] = [];

export function createUser(data: CreateUserRequest): User {
  const user: User = {
    id: String(users.length + 1),
    ...data,
    createdAt: new Date(),
  };
  users.push(user);
  return user;
}

export function getUserById(id: string): User | undefined {
  return users.find((u) => u.id === id);
}

// src/routes/users.ts
import { Router, Request, Response } from 'express';
import { createUser, getUserById } from '../services/user-service';

const router = Router();

router.post('/', (req: Request, res: Response) => {
  const user = createUser(req.body);
  res.status(201).json(user);
});

router.get('/:id', (req: Request, res: Response) => {
  const user = getUserById(req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

export default router;

// src/app.ts
import express from 'express';
import userRoutes from './routes/users';
import { errorHandler } from './middleware/error';

const app = express();
app.use(express.json());
app.use('/users', userRoutes);
app.use(errorHandler);

export default app;

Common Mistakes

1. Forgetting to parse JSON body

Without app.use(express.json()), req.body is undefined. TypeScript won't warn you because req.body is typed as any by default.

2. Not validating request data

TypeScript types are compile-time only. A Request<{}, {}, CreateUserBody> assertion doesn't validate at runtime — use Zod or similar.

3. Wrong error handler signature

Express error handlers must have exactly four parameters. Omitting next makes it a regular middleware that won't catch errors.

4. Not handling async errors

Express doesn't catch promise rejections automatically. Wrap async handlers:

import { Request, Response, NextFunction } from 'express';

function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise<void>) {
  return (req: Request, res: Response, next: NextFunction) => {
    fn(req, res, next).catch(next);
  };
}

5. Overusing any for request types

Typing req.body as any defeats the purpose of TypeScript. Always define interfaces or use Zod inference.

6. Forgetting CORS configuration

Browser-based clients need CORS headers. Install @types/cors and configure it properly.

7. Not using environment variables

Hardcoding config values leads to security issues. Use dotenv with typed config objects.

Practice Questions

  1. What generics does Request<Params, ResBody, ReqBody, Query> accept? The four type parameters define params, response body, request body, and query string types respectively.

  2. How do you add a custom property like req.user to Express's Request? Use global augmentation: declare global { namespace Express { interface Request { user?: User } } }.

  3. What's the difference between z.infer and defining a separate interface? z.infer derives the type from the Zod schema, keeping validation and types in sync. Separate interfaces can drift out of sync.

  4. Why does Express error middleware need four parameters? Express identifies error handlers by their function signature length. Three parameters = regular middleware, four = error handler.

  5. How do you handle async errors without wrapping every route? Create an asyncHandler wrapper function, or use a library like express-async-errors.

Challenge

Build a typed Express API for a todo app with CRUD operations, Zod validation, error handling middleware, and typed service layer.

FAQ

Do I need @types/express for TypeScript with Express?

Yes. @types/express provides type definitions for Request, Response, NextFunction, Router, and other Express APIs. Without it, you'll get implicit any errors.

Should I use Zod or io-ts for validation?

Both work, but Zod is simpler to learn and integrates more naturally with TypeScript. Its z.infer pattern is widely adopted in the community.

How do I type req.query parameters?

Use the fourth generic parameter: Request<{}, {}, {}, { page?: string }>. But queries are always strings — use Zod for Parsing and transforming types like numbers.

Can I use TypeScript with Express middleware like Passport?

Yes. Most popular Express middleware packages have @types/* packages (e.g., @types/passport, @types/express-session), or include their own type definitions.

What's the difference between ts-node and tsx?

tsx is faster (uses Esbuild) and supports ESM natively. ts-node is older and slower. For new projects, use tsx.

How do I type file uploads with multer?

Use @types/multer and access req.file (single) or req.files (multiple). The types are provided by the multer type definitions.

Mini Project

Build a contact book API with Express and TypeScript:

  • POST /contacts — create contact (name, email, phone, Zod validated)
  • GET /contacts — list all contacts (typed response)
  • GET /contacts/:id — get contact by ID
  • PUT /contacts/:id — update contact
  • DELETE /contacts/:id — delete contact
  • Error middleware — consistent error responses
  • Auth middleware — simple token check on protected routes

Implement a typed in-memory data store with full CRUD operations.

What's Next

Now that you can build typed Express APIs, you're ready to explore the TypeScript ecosystem deeper. Next, learn how to build full-stack applications with {{< ref "45-nextjs" >}}, or dive into structured backend architecture with {{< ref "46-nestjs" >}}.

You can also explore {{< ref "47-database-access" >}} to connect your Express API to a real database.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro