nodejs-backend-typescript

TypeScript backend development with Express/Fastify, routing, middleware, and database integration. Covers both Express and Fastify frameworks with complete server setup, routing patterns, middleware implementation, and error handling examples Includes request validation using Zod and TypeBox, JWT and session-based authentication, and integration with Drizzle ORM and Prisma Provides REST API design patterns for pagination, filtering, sorting, and standardized error responses with type-safe environment configuration Features testing setup with Vitest and Supertest, Docker deployment, PM2 clustering, and production best practices with recommended project structure

INSTALLATION
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill nodejs-backend-typescript
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Node.js Backend Development with TypeScript

progressive_disclosure: entry_point: summary: "TypeScript backend patterns with Express/Fastify, routing, middleware, database integration" when_to_use: - "When building REST APIs with TypeScript" - "When creating Express/Fastify servers" - "When needing server-side TypeScript" - "When building microservices" quick_start: - "npm init -y && npm install -D typescript @types/node tsx" - "npm install express @types/express zod" - "Create tsconfig.json with strict mode" - "npm run dev" token_estimate: entry: 75 full: 4700

TypeScript Setup

Essential Configuration

tsconfig.json (strict mode recommended):

{

  "compilerOptions": {

    "target": "ES2022",

    "module": "NodeNext",

    "moduleResolution": "NodeNext",

    "outDir": "./dist",

    "rootDir": "./src",

    "strict": true,

    "esModuleInterop": true,

    "skipLibCheck": true,

    "forceConsistentCasingInFileNames": true,

    "resolveJsonModule": true,

    "types": ["node"]

  },

  "include": ["src/**/*"],

  "exclude": ["node_modules", "dist"]

}

package.json scripts:

{

  "scripts": {

    "dev": "tsx watch src/server.ts",

    "build": "tsc",

    "start": "node dist/server.js",

    "test": "vitest"

  }

}

Development Dependencies

npm install -D typescript @types/node tsx vitest

npm install -D @types/express  # or @types/node (Fastify has built-in types)

Express Patterns

Basic Express Server

src/server.ts:

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

import { z } from 'zod';

const app = express();

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

// Middleware

app.use(express.json());

app.use(express.urlencoded({ extended: true }));

// Type-safe request handlers

interface TypedRequest<T> extends Request {

  body: T;

}

// Routes

app.get('/health', (req: Request, res: Response) => {

  res.json({ status: 'ok', timestamp: new Date().toISOString() });

});

// Start server

app.listen(port, () => {

  console.log(`Server running on port ${port}`);

});

Router Pattern

src/routes/users.ts:

import { Router } from 'express';

import { z } from 'zod';

import { validateRequest } from '../middleware/validation';

const router = Router();

const createUserSchema = z.object({

  email: z.string().email(),

  name: z.string().min(2),

  age: z.number().int().positive().optional(),

});

router.post(

  '/users',

  validateRequest(createUserSchema),

  async (req, res, next) => {

    try {

      const userData = req.body; // Type-safe after validation

      // Database insert logic

      res.status(201).json({ id: 1, ...userData });

    } catch (error) {

      next(error);

    }

  }

);

export default router;

Middleware Patterns

src/middleware/validation.ts:

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

import { z, ZodSchema } from 'zod';

export const validateRequest = (schema: ZodSchema) => {

  return (req: Request, res: Response, next: NextFunction) => {

    try {

      req.body = schema.parse(req.body);

      next();

    } catch (error) {

      if (error instanceof z.ZodError) {

        res.status(400).json({

          error: 'Validation failed',

          details: error.errors,

        });

      } else {

        next(error);

      }

    }

  };

};

src/middleware/auth.ts:

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

import jwt from 'jsonwebtoken';

interface JwtPayload {

  userId: string;

  email: string;

}

declare global {

  namespace Express {

    interface Request {

      user?: JwtPayload;

    }

  }

}

export const authenticate = (

  req: Request,

  res: Response,

  next: NextFunction

) => {

  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {

    return res.status(401).json({ error: 'No token provided' });

  }

  try {

    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;

    req.user = decoded;

    next();

  } catch (error) {

    res.status(401).json({ error: 'Invalid token' });

  }

};

Error Handling

src/middleware/errorHandler.ts:

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

export class AppError extends Error {

  constructor(

    public statusCode: number,

    message: string,

    public isOperational = true

  ) {

    super(message);

    Object.setPrototypeOf(this, AppError.prototype);

  }

}

export const errorHandler = (

  err: Error,

  req: Request,

  res: Response,

  next: NextFunction

) => {

  if (err instanceof AppError) {

    return res.status(err.statusCode).json({

      error: err.message,

      ...(process.env.NODE_ENV === 'development' &#x26;&#x26; { stack: err.stack }),

    });

  }

  console.error('Unexpected error:', err);

  res.status(500).json({

    error: 'Internal server error',

    ...(process.env.NODE_ENV === 'development' &#x26;&#x26; {

      message: err.message,

      stack: err.stack,

    }),

  });

};

Fastify Patterns

Basic Fastify Server

src/server.ts:

import Fastify from 'fastify';

import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';

import { Type } from '@sinclair/typebox';

const fastify = Fastify({

  logger: {

    level: process.env.LOG_LEVEL || 'info',

  },

}).withTypeProvider<TypeBoxTypeProvider>();

// Type-safe route with schema validation

fastify.route({

  method: 'POST',

  url: '/users',

  schema: {

    body: Type.Object({

      email: Type.String({ format: 'email' }),

      name: Type.String({ minLength: 2 }),

      age: Type.Optional(Type.Integer({ minimum: 0 })),

    }),

    response: {

      201: Type.Object({

        id: Type.Number(),

        email: Type.String(),

        name: Type.String(),

      }),

    },

  },

  handler: async (request, reply) => {

    const { email, name, age } = request.body;

    // Auto-typed and validated

    return reply.status(201).send({ id: 1, email, name });

  },

});

const start = async () => {

  try {

    await fastify.listen({ port: 3000, host: '0.0.0.0' });

  } catch (err) {

    fastify.log.error(err);

    process.exit(1);

  }

};

start();

Plugin Pattern

src/plugins/database.ts:

import { FastifyPluginAsync } from 'fastify';

import fp from 'fastify-plugin';

import { drizzle } from 'drizzle-orm/node-postgres';

import { Pool } from 'pg';

declare module 'fastify' {

  interface FastifyInstance {

    db: ReturnType<typeof drizzle>;

  }

}

const databasePlugin: FastifyPluginAsync = async (fastify) => {

  const pool = new Pool({

    connectionString: process.env.DATABASE_URL,

  });

  const db = drizzle(pool);

  fastify.decorate('db', db);

  fastify.addHook('onClose', async () => {

    await pool.end();

  });

};

export default fp(databasePlugin);

Hooks Pattern

src/hooks/auth.ts:

import { FastifyRequest, FastifyReply } from 'fastify';

import jwt from 'jsonwebtoken';

declare module 'fastify' {

  interface FastifyRequest {

    user?: {

      userId: string;

      email: string;

    };

  }

}

export const authHook = async (

  request: FastifyRequest,

  reply: FastifyReply

) => {

  const token = request.headers.authorization?.replace('Bearer ', '');

  if (!token) {

    return reply.status(401).send({ error: 'No token provided' });

  }

  try {

    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as {

      userId: string;

      email: string;

    };

    request.user = decoded;

  } catch (error) {

    return reply.status(401).send({ error: 'Invalid token' });

  }

};

Request Validation

Zod with Express

import { z } from 'zod';

const userSchema = z.object({

  email: z.string().email(),

  password: z.string().min(8),

  profile: z.object({

    firstName: z.string(),

    lastName: z.string(),

    age: z.number().int().positive(),

  }),

  tags: z.array(z.string()).optional(),

});

type CreateUserInput = z.infer<typeof userSchema>;

router.post('/users', async (req, res) => {

  const result = userSchema.safeParse(req.body);

  if (!result.success) {

    return res.status(400).json({

      error: 'Validation failed',

      details: result.error.format(),

    });

  }

  const user: CreateUserInput = result.data;

  // Type-safe user object

});

TypeBox with Fastify

import { Type, Static } from '@sinclair/typebox';

const UserSchema = Type.Object({

  email: Type.String({ format: 'email' }),

  password: Type.String({ minLength: 8 }),

  profile: Type.Object({

    firstName: Type.String(),

    lastName: Type.String(),

    age: Type.Integer({ minimum: 0 }),

  }),

  tags: Type.Optional(Type.Array(Type.String())),

});

type User = Static<typeof UserSchema>;

fastify.post('/users', {

  schema: { body: UserSchema },

  handler: async (request, reply) => {

    const user: User = request.body; // Auto-validated

    return { id: 1, ...user };

  },

});

Authentication

JWT Authentication

src/services/auth.ts:

import jwt from 'jsonwebtoken';

import bcrypt from 'bcrypt';

interface TokenPayload {

  userId: string;

  email: string;

}

export class AuthService {

  private static JWT_SECRET = process.env.JWT_SECRET!;

  private static JWT_EXPIRES_IN = '7d';

  static async hashPassword(password: string): Promise<string> {

    return bcrypt.hash(password, 10);

  }

  static async comparePassword(

    password: string,

    hash: string

  ): Promise<boolean> {

    return bcrypt.compare(password, hash);

  }

  static generateToken(payload: TokenPayload): string {

    return jwt.sign(payload, this.JWT_SECRET, {

      expiresIn: this.JWT_EXPIRES_IN,

    });

  }

  static verifyToken(token: string): TokenPayload {

    return jwt.verify(token, this.JWT_SECRET) as TokenPayload;

  }

}

Session-based Auth (Express)

import session from 'express-session';

import RedisStore from 'connect-redis';

import { createClient } from 'redis';

const redisClient = createClient({

  url: process.env.REDIS_URL,

});

redisClient.connect();

app.use(

  session({

    store: new RedisStore({ client: redisClient }),

    secret: process.env.SESSION_SECRET!,

    resave: false,

    saveUninitialized: false,

    cookie: {

      secure: process.env.NODE_ENV === 'production',

      httpOnly: true,

      maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days

    },

  })

);

declare module 'express-session' {

  interface SessionData {

    userId: string;

  }

}

Database Integration

Drizzle ORM

src/db/schema.ts:

import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {

  id: serial('id').primaryKey(),

  email: varchar('email', { length: 255 }).notNull().unique(),

  name: varchar('name', { length: 255 }).notNull(),

  passwordHash: varchar('password_hash', { length: 255 }).notNull(),

  createdAt: timestamp('created_at').defaultNow().notNull(),

});

export type User = typeof users.$inferSelect;

export type NewUser = typeof users.$inferInsert;

src/db/client.ts:

import { drizzle } from 'drizzle-orm/node-postgres';

import { Pool } from 'pg';

import * as schema from './schema';

const pool = new Pool({

  connectionString: process.env.DATABASE_URL,

});

export const db = drizzle(pool, { schema });

src/repositories/userRepository.ts:

import { eq } from 'drizzle-orm';

import { db } from '../db/client';

import { users, NewUser } from '../db/schema';

export class UserRepository {

  static async create(data: NewUser) {

    const [user] = await db.insert(users).values(data).returning();

    return user;

  }

  static async findByEmail(email: string) {

    return db.query.users.findFirst({

      where: eq(users.email, email),

    });

  }

  static async findById(id: number) {

    return db.query.users.findFirst({

      where: eq(users.id, id),

    });

  }

  static async list(limit = 10, offset = 0) {

    return db.query.users.findMany({

      limit,

      offset,

      columns: {

        passwordHash: false, // Exclude sensitive fields

      },

    });

  }

}

Prisma

prisma/schema.prisma:

datasource db {

  provider = "postgresql"

  url      = env("DATABASE_URL")

}

generator client {

  provider = "prisma-client-js"

}

model User {

  id           Int      @id @default(autoincrement())

  email        String   @unique

  name         String

  passwordHash String   @map("password_hash")

  createdAt    DateTime @default(now()) @map("created_at")

  posts        Post[]

  @@map("users")

}

model Post {

  id        Int      @id @default(autoincrement())

  title     String

  content   String?

  published Boolean  @default(false)

  authorId  Int      @map("author_id")

  author    User     @relation(fields: [authorId], references: [id])

  createdAt DateTime @default(now()) @map("created_at")

  @@map("posts")

}

src/services/userService.ts:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export class UserService {

  static async createUser(data: { email: string; name: string; password: string }) {

    const passwordHash = await AuthService.hashPassword(data.password);

    return prisma.user.create({

      data: {

        email: data.email,

        name: data.name,

        passwordHash,

      },

      select: {

        id: true,

        email: true,

        name: true,

        createdAt: true,

      },

    });

  }

  static async getUserWithPosts(userId: number) {

    return prisma.user.findUnique({

      where: { id: userId },

      include: {

        posts: {

          where: { published: true },

          orderBy: { createdAt: 'desc' },

        },

      },

    });

  }

}

API Design

REST API Patterns

Pagination:

import { z } from 'zod';

const paginationSchema = z.object({

  page: z.coerce.number().int().positive().default(1),

  limit: z.coerce.number().int().positive().max(100).default(20),

});

router.get('/users', async (req, res) => {

  const { page, limit } = paginationSchema.parse(req.query);

  const offset = (page - 1) * limit;

  const [users, total] = await Promise.all([

    UserRepository.list(limit, offset),

    UserRepository.count(),

  ]);

  res.json({

    data: users,

    pagination: {

      page,

      limit,

      total,

      totalPages: Math.ceil(total / limit),

    },

  });

});

Filtering and Sorting:

const filterSchema = z.object({

  status: z.enum(['active', 'inactive']).optional(),

  search: z.string().optional(),

  sortBy: z.enum(['createdAt', 'name', 'email']).default('createdAt'),

  sortOrder: z.enum(['asc', 'desc']).default('desc'),

});

router.get('/users', async (req, res) => {

  const filters = filterSchema.parse(req.query);

  const users = await db.query.users.findMany({

    where: and(

      filters.status &#x26;&#x26; eq(users.status, filters.status),

      filters.search &#x26;&#x26; ilike(users.name, `%${filters.search}%`)

    ),

    orderBy: [

      filters.sortOrder === 'asc'

        ? asc(users[filters.sortBy])

        : desc(users[filters.sortBy]),

    ],

  });

  res.json({ data: users });

});

Error Response Format

interface ErrorResponse {

  error: string;

  message: string;

  statusCode: number;

  details?: unknown;

  timestamp: string;

  path: string;

}

export const formatError = (

  err: AppError,

  req: Request

): ErrorResponse => ({

  error: err.name,

  message: err.message,

  statusCode: err.statusCode,

  ...(err.details &#x26;&#x26; { details: err.details }),

  timestamp: new Date().toISOString(),

  path: req.path,

});

Environment Configuration

Type-safe Environment Variables

src/config/env.ts:

import { z } from 'zod';

const envSchema = z.object({

  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),

  PORT: z.coerce.number().default(3000),

  DATABASE_URL: z.string().url(),

  REDIS_URL: z.string().url(),

  JWT_SECRET: z.string().min(32),

  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),

});

export type Env = z.infer<typeof envSchema>;

export const env = envSchema.parse(process.env);

Usage:

import { env } from './config/env';

const port = env.PORT; // Type-safe, validated

Testing

Vitest Setup

vitest.config.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({

  test: {

    globals: true,

    environment: 'node',

    setupFiles: ['./src/tests/setup.ts'],

    coverage: {

      provider: 'v8',

      reporter: ['text', 'json', 'html'],

    },

  },

});

Integration Tests with Supertest

src/tests/users.test.ts:

import { describe, it, expect, beforeAll, afterAll } from 'vitest';

import request from 'supertest';

import { app } from '../server';

import { db } from '../db/client';

describe('User API', () => {

  beforeAll(async () => {

    // Setup test database

    await db.delete(users);

  });

  afterAll(async () => {

    // Cleanup

  });

  it('should create a new user', async () => {

    const response = await request(app)

      .post('/users')

      .send({

        email: 'test@example.com',

        name: 'Test User',

        password: 'password123',

      })

      .expect(201);

    expect(response.body).toMatchObject({

      email: 'test@example.com',

      name: 'Test User',

    });

    expect(response.body).toHaveProperty('id');

    expect(response.body).not.toHaveProperty('passwordHash');

  });

  it('should return 400 for invalid email', async () => {

    const response = await request(app)

      .post('/users')

      .send({

        email: 'invalid-email',

        name: 'Test User',

        password: 'password123',

      })

      .expect(400);

    expect(response.body).toHaveProperty('error');

  });

});

Unit Tests

src/services/auth.test.ts:

import { describe, it, expect } from 'vitest';

import { AuthService } from './auth';

describe('AuthService', () => {

  it('should hash password correctly', async () => {

    const password = 'mySecurePassword123';

    const hash = await AuthService.hashPassword(password);

    expect(hash).not.toBe(password);

    expect(hash.length).toBeGreaterThan(50);

  });

  it('should verify password correctly', async () => {

    const password = 'mySecurePassword123';

    const hash = await AuthService.hashPassword(password);

    const isValid = await AuthService.comparePassword(password, hash);

    expect(isValid).toBe(true);

    const isInvalid = await AuthService.comparePassword('wrongPassword', hash);

    expect(isInvalid).toBe(false);

  });

  it('should generate valid JWT token', () => {

    const token = AuthService.generateToken({

      userId: '123',

      email: 'test@example.com',

    });

    expect(token).toBeTruthy();

    const decoded = AuthService.verifyToken(token);

    expect(decoded).toMatchObject({

      userId: '123',

      email: 'test@example.com',

    });

  });

});

Production Deployment

Docker Setup

Dockerfile:

FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY . .

RUN npm run build

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci --omit=dev

COPY --from=builder /app/dist ./dist

ENV NODE_ENV=production

EXPOSE 3000

CMD ["node", "dist/server.js"]

docker-compose.yml:

version: '3.8'

services:

  app:

    build: .

    ports:

      - "3000:3000"

    environment:

      - DATABASE_URL=postgresql://user:pass@db:5432/mydb

      - REDIS_URL=redis://redis:6379

      - JWT_SECRET=${JWT_SECRET}

    depends_on:

      - db

      - redis

  db:

    image: postgres:16-alpine

    environment:

      - POSTGRES_USER=user

      - POSTGRES_PASSWORD=pass

      - POSTGRES_DB=mydb

    volumes:

      - postgres_data:/var/lib/postgresql/data

  redis:

    image: redis:7-alpine

    volumes:

      - redis_data:/data

volumes:

  postgres_data:

  redis_data:

PM2 Clustering

ecosystem.config.js:

module.exports = {

  apps: [{

    name: 'api',

    script: './dist/server.js',

    instances: 'max',

    exec_mode: 'cluster',

    env: {

      NODE_ENV: 'production',

    },

    error_file: './logs/err.log',

    out_file: './logs/out.log',

    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',

  }],

};

Best Practices

Project Structure

src/

├── server.ts              # Entry point

├── config/

│   └── env.ts            # Environment config

├── routes/

│   ├── index.ts          # Route aggregator

│   ├── users.ts

│   └── posts.ts

├── middleware/

│   ├── auth.ts

│   ├── validation.ts

│   └── errorHandler.ts

├── services/

│   ├── auth.ts

│   └── user.ts

├── repositories/

│   └── userRepository.ts

├── db/

│   ├── client.ts

│   └── schema.ts

├── types/

│   └── index.ts

└── tests/

    ├── setup.ts

    ├── users.test.ts

    └── auth.test.ts

Key Principles

  • Separation of Concerns: Routes → Controllers → Services → Repositories
  • Type Safety: Use TypeScript strict mode, Zod for runtime validation
  • Error Handling: Centralized error handler, custom error classes
  • Security: Helmet, rate limiting, input validation, CORS
  • Logging: Structured logging (pino, winston), request IDs
  • Testing: Unit tests for services, integration tests for APIs
  • Documentation: OpenAPI/Swagger for API documentation

Express vs Fastify

Use Express when:

  • Large ecosystem of middleware needed
  • Team familiarity is priority
  • Prototype/MVP development
  • Legacy codebase compatibility

Use Fastify when:

  • Performance is critical (2-3x faster)
  • Type safety is important (built-in TypeScript support)
  • Schema validation required (JSON Schema built-in)
  • Modern async/await patterns preferred
  • Plugin architecture needed

Performance Tips

  • Use connection pooling for databases
  • Implement caching (Redis, in-memory)
  • Enable compression (gzip, brotli)
  • Use clustering for CPU-intensive tasks
  • Implement rate limiting
  • Optimize database queries (indexes, query analysis)
  • Use CDN for static assets
  • Enable HTTP/2 in production
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card