express-typescript-api-best-practices

Professional-grade REST API architecture with Express.js and TypeScript following SOLID principles, layered architecture, transaction management, JWT authentication with role-based authorization (RBAC), input validation with Zod, OpenAPI/Swagger documentation, standardized response format, and production-ready patterns. Use when building or refactoring REST APIs with Express + TypeScript that require enterprise-level code quality, maintainability, scalability, and security.

16 stars

Best use case

express-typescript-api-best-practices is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Professional-grade REST API architecture with Express.js and TypeScript following SOLID principles, layered architecture, transaction management, JWT authentication with role-based authorization (RBAC), input validation with Zod, OpenAPI/Swagger documentation, standardized response format, and production-ready patterns. Use when building or refactoring REST APIs with Express + TypeScript that require enterprise-level code quality, maintainability, scalability, and security.

Teams using express-typescript-api-best-practices should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/express-typescript-api-best-practices/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/development/express-typescript-api-best-practices/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/express-typescript-api-best-practices/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How express-typescript-api-best-practices Compares

Feature / Agentexpress-typescript-api-best-practicesStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Professional-grade REST API architecture with Express.js and TypeScript following SOLID principles, layered architecture, transaction management, JWT authentication with role-based authorization (RBAC), input validation with Zod, OpenAPI/Swagger documentation, standardized response format, and production-ready patterns. Use when building or refactoring REST APIs with Express + TypeScript that require enterprise-level code quality, maintainability, scalability, and security.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

Related Guides

SKILL.md Source

# Express + TypeScript REST API Best Practices

Professional skill for building production-ready REST APIs with Express.js and TypeScript, following SOLID principles and enterprise-grade architectural patterns.

## Core Principles

### 1. **Layered Architecture (Separation of Concerns)**

Follow strict layer separation to achieve **Single Responsibility Principle (SRP)** and **Dependency Inversion Principle (DIP)**:

```
Routes → Controllers → Services → Models → Database
```

**Layer Responsibilities:**

- **Routes** (`src/v1/routes/`): Define HTTP endpoints, apply middleware
- **Controllers** (`src/controllers/`): Handle HTTP request/response, format data
- **Services** (`src/services/`): Contain business logic, orchestrate transactions
- **Models** (`src/models/`): Define data structures (ORM models)
- **Middlewares** (`src/middlewares/`): Cross-cutting concerns (auth, validation)

**Implementation Rules:**

```typescript
// ❌ BAD: Business logic in controller
export const createUser = async (req: Request, res: Response) => {
  const hashedPassword = await argon2.hash(req.body.password);
  const user = await User.create({ ...req.body, password: hashedPassword });
  res.json(user);
};

// ✅ GOOD: Controller delegates to service
export const createUser = async (req: Request, res: Response) => {
  try {
    const user = await crearUsuario(req.body);
    res.status(201).json({
      status: "ok",
      message: "Usuario creado correctamente",
      data: user
    });
  } catch (error: any) {
    res.status(500).json({
      status: "error",
      message: "Error al crear usuario",
      error: error.message
    });
  }
};
```

### 2. **Standardized API Response Format**

ALL endpoints MUST return consistent response structure:

```typescript
// Success response
{
  status: "ok",
  message: string,
  data?: any  // Optional
}

// Error response
{
  status: "error",
  message: string,
  error?: string,     // Detailed error
  code?: string,      // Error code (e.g., "TOKEN_INVALID")
  detalles?: object   // Additional context
}
```

**HTTP Status Codes:**
- `200` - Success (GET, PUT, DELETE)
- `201` - Resource created (POST)
- `400` - Validation error
- `401` - Unauthorized (missing/invalid token)
- `403` - Forbidden (insufficient permissions)
- `404` - Resource not found
- `500` - Server error

### 3. **Transaction Management Pattern**

For operations involving multiple database changes, ALWAYS use transactions:

```typescript
export const crearDetalleVenta = async (detalleVentaData: any[]) => {
  const transaction = await sequelize.transaction();

  try {
    // 1. Validate input
    if (!Array.isArray(detalleVentaData) || detalleVentaData.length === 0) {
      throw new Error('detalleVentaData debe ser un array no vacío');
    }

    // 2. Batch load related data (optimization)
    const productoIds = [...new Set(detalleVentaData.map(i => i.producto_id))];
    const productos = await Producto.findAll({
      where: { id: productoIds },
      transaction
    });
    const productoMap = new Map(productos.map(p => [p.id, p]));

    // 3. Process each item with business rules
    for (const item of detalleVentaData) {
      const producto = productoMap.get(item.producto_id);
      if (!producto) throw new Error(`Producto ${item.producto_id} no encontrado`);

      // Auto-fill fields
      item.precio_unitario = item.precio_unitario ?? Number(producto.precio_minorista);
      item.sub_total = item.sub_total ?? parseFloat((item.precio_unitario * item.cantidad).toFixed(2));

      // Create related records (stock movement)
      const movimiento = await crearMovimiento({
        tipo: "salida",
        producto_id: item.producto_id,
        almacen_id: item.almacen_id,
        cantidad: item.cantidad,
        precio_unitario: item.precio_unitario,
        descripcion: `Salida por venta ID: ${item.venta_id}`
      }, transaction);

      item.movimiento_id = movimiento.id;
    }

    // 4. Bulk insert (performance)
    const detalles = await DetalleVenta.bulkCreate(detalleVentaData, { transaction });

    // 5. Update parent record with locking
    const venta = await Venta.findOne({
      where: { id: detalleVentaData[0].venta_id },
      transaction,
      lock: transaction.LOCK.UPDATE  // Prevent race conditions
    });

    if (!venta) throw new Error('Venta no encontrada');

    const totalSubtotales = detalleVentaData.reduce((sum, d) => sum + d.sub_total, 0);
    if (venta.total === 0 || venta.total < totalSubtotales) {
      venta.total = totalSubtotales;
      await venta.save({ transaction });
    }

    await transaction.commit();
    return detalles;
  } catch (error) {
    await transaction.rollback();
    console.error('Error en transacción:', error);
    throw error;
  }
};
```

**Transaction Best Practices:**
- Pass transaction to ALL database operations within the scope
- Use optimistic locking (`lock: transaction.LOCK.UPDATE`) for concurrent updates
- Batch load related data BEFORE the loop (N+1 query prevention)
- Always rollback on error, commit on success
- Validate ALL input at the beginning

### 4. **JWT Authentication & Role-Based Authorization (RBAC)**

**Authentication Middleware (`autenticarToken`):**

```typescript
export const autenticarToken = (req: AuthRequest, res: Response, next: NextFunction): void => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

  if (!token) {
    res.status(401).json({
      status: "error",
      message: "Acceso no autorizado",
      error: "No se proporcionó un token de autenticación",
      code: "TOKEN_NOT_PROVIDED"
    });
    return;
  }

  try {
    const payload = verificarToken(token);
    req.usuario = payload;  // Attach user to request
    next();
  } catch (error) {
    res.status(403).json({
      status: "error",
      message: "Token inválido",
      error: "El token ha expirado o es inválido",
      code: "TOKEN_INVALID"
    });
  }
};
```

**Authorization Middleware (`verificarRol`):**

```typescript
export const verificarRol = (...rolesPermitidos: string[]) => {
  return async (req: AuthRequest, res: Response, next: NextFunction): Promise<void> => {
    if (!req.usuario) {
      res.status(401).json({
        status: "error",
        message: "Autenticación requerida",
        code: "AUTHENTICATION_REQUIRED"
      });
      return;
    }

    // Cache roles in request to avoid multiple DB queries
    if (!req.usuario.roles) {
      const rolesAsignados = await UsuarioRol.findAll({
        where: { usuario_id: req.usuario.id },
        include: [{ model: Rol, as: "rol" }]
      });
      req.usuario.roles = rolesAsignados.map((ur: any) => ur.rol.nombre);
    }

    const tienePermiso = (req.usuario.roles || []).some(rol =>
      rolesPermitidos.includes(rol)
    );

    if (!tienePermiso) {
      res.status(403).json({
        status: "error",
        message: "Permisos insuficientes",
        error: `Requiere rol: ${formatearRoles(rolesPermitidos)}`,
        code: "INSUFFICIENT_PERMISSIONS",
        detalles: {
          rolesRequeridos: rolesPermitidos,
          usuario: req.usuario.username
        }
      });
      return;
    }

    next();
  };
};
```

**Usage in Routes:**

```typescript
// Apply globally to all routes
app.use("/v1/usuarios", autenticarToken, usuariosRouter);

// Apply per-route with role checking
router.post("/", verificarRol("admin"), createUsuario);
router.get("/", verificarRol("admin", "gerente"), getUsuarios);
```

**Many-to-Many Role System:**
- Users can have multiple roles (flexibility)
- Roles cached in request object (performance)
- Database-driven (no hardcoded roles)

### 5. **Input Validation with Zod**

Define schemas in `src/schemas/`:

```typescript
import { z } from "zod";

export const createUsuarioSchema = z.object({
  body: z.object({
    username: z.string()
      .min(3, "Username debe tener al menos 3 caracteres")
      .max(50, "Username no puede exceder 50 caracteres"),
    email: z.string()
      .email("Email inválido")
      .max(100),
    password: z.string()
      .min(8, "Contraseña debe tener al menos 8 caracteres")
      .max(100),
    rol_id: z.number().int().positive().optional(),
    mustChangePassword: z.boolean().optional()
  }),
  params: z.object({}),
  query: z.object({})
});

export type CreateUsuarioInput = z.infer<typeof createUsuarioSchema>;
```

**Validation Middleware:**

```typescript
const validate = (schema: ZodObject<any, any>) => {
  return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const result = await schema.safeParseAsync({
        body: req.body,
        query: req.query,
        params: req.params
      });

      if (!result.success) {
        const formattedErrors = result.error.issues.map(error => ({
          field: error.path[1] || error.path[0],
          message: error.message
        }));

        res.status(400).json({
          status: "error",
          message: "Error de validación",
          errors: formattedErrors
        });
        return;
      }

      // Replace with validated data
      if (result.data.body) req.body = result.data.body;
      if (result.data.query) req.query = result.data.query as any;
      if (result.data.params) req.params = result.data.params as any;

      next();
    } catch (error) {
      res.status(500).json({
        status: "error",
        message: "Error interno en validación"
      });
    }
  };
};
```

**Usage:**

```typescript
router.post("/", validate(createUsuarioSchema), createUsuario);
```

### 6. **Dynamic Data Inclusion Pattern**

Support flexible data loading via query parameters:

```typescript
// Controller: Parse query params
function construirInclusions(include: string) {
  return String(include || "")
    .split(",")
    .map(s => s.trim())
    .filter(Boolean);
}

export const getUsuarios = async (req: Request, res: Response) => {
  const includeList = construirInclusions(req.query.include as string);
  const usuarios = await obtenerUsuarios({ include: includeList });
  res.json({ status: "ok", data: usuarios });
};

// Service: Build Sequelize includes
type Opts = {
  include?: string[];
}

const construirInclusions = (opts: Opts = {}): any[] => {
  const include: any[] = [];

  if (opts.include?.includes("roles")) {
    include.push({
      model: Rol,
      as: "roles",
      through: { attributes: [] }  // Hide join table
    });
  }

  if (opts.include?.includes("almacen")) {
    include.push({ model: Almacen, as: "almacen" });
  }

  return include;
};

export const obtenerUsuarios = async (opts: Opts = {}) => {
  const include = construirInclusions(opts);
  return await Usuario.findAll({ include });
};
```

**Usage:**
```
GET /v1/usuarios?include=roles
GET /v1/detalle_ventas?include=producto,almacen
```

### 7. **OpenAPI/Swagger Documentation**

Configure comprehensive API documentation:

```typescript
// src/config/swagger.ts
const swaggerDefinition = {
  openapi: '3.0.0',
  info: {
    title: 'API Sistema',
    version: '1.0.0',
    description: 'REST API con autenticación JWT y RBAC'
  },
  servers: [
    { url: `http://${SERVER_HOST}:${SERVER_PORT}`, description: 'Desarrollo' }
  ],
  components: {
    securitySchemes: {
      BearerAuth: {
        type: 'http',
        scheme: 'bearer',
        bearerFormat: 'JWT'
      }
    },
    schemas: {
      SuccessResponse: {
        type: 'object',
        properties: {
          status: { type: 'string', enum: ['ok'] },
          message: { type: 'string' },
          data: { type: 'object' }
        }
      },
      ErrorResponse: {
        type: 'object',
        properties: {
          status: { type: 'string', enum: ['error'] },
          message: { type: 'string' },
          error: { type: 'string' },
          code: { type: 'string' }
        }
      }
    }
  },
  paths: {
    '/v1/usuarios': {
      get: {
        tags: ['Usuarios'],
        security: [{ BearerAuth: [] }],
        responses: {
          200: { description: 'Success' },
          401: { $ref: '#/components/responses/UnauthorizedError' }
        }
      }
    }
  }
};
```

**Mount in Express:**

```typescript
import swaggerUi from "swagger-ui-express";
import { swaggerSpec } from "./config/swagger";

app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
  customSiteTitle: "API Documentation",
  customCss: '.swagger-ui .topbar { display: none }'
}));
```

### 8. **Security Best Practices**

**Password Hashing (Argon2):**

```typescript
import argon2 from "argon2";

// Hashing
const hashedPassword = await argon2.hash(password);

// Verification
const isValid = await argon2.verify(hashedPassword, password);
```

**Environment Variables:**

```typescript
// src/config/config.ts
import dotenv from "dotenv";
dotenv.config();

export const SERVER_HOST = process.env.SERVER_HOST || "localhost";
export const SERVER_PORT = Number(process.env.SERVER_PORT) || 3000;
export const DB_HOST = process.env.DB_HOST || "localhost";
export const DB_USER = process.env.DB_USER!;
export const DB_PASS = process.env.DB_PASS!;
export const DB_NAME = process.env.DB_NAME!;
```

**JWT Configuration:**

```typescript
// src/config/jwt.ts
import jwt from "jsonwebtoken";

const SECRET_KEY = process.env.JWT_SECRET || "your-secret-key";

export const generarToken = (payload: any): string => {
  return jwt.sign(payload, SECRET_KEY, { expiresIn: "1h" });
};

export const verificarToken = (token: string): any => {
  return jwt.verify(token, SECRET_KEY);
};
```

### 9. **Project Structure**

```
src/
├── config/
│   ├── db.ts              # Database connection
│   ├── config.ts          # Environment variables
│   ├── jwt.ts             # JWT utilities
│   └── swagger.ts         # OpenAPI definition
├── models/
│   ├── usuarios/
│   │   ├── usuarios.ts
│   │   ├── roles.ts
│   │   ├── usuarios_roles.ts
│   │   └── associations.ts  # Model relationships
│   ├── productos/
│   └── ventas/
├── controllers/
│   ├── usuarios/
│   │   └── usuarios.controller.ts
│   └── ventas/
├── services/
│   ├── usuarios/
│   │   ├── usuarios.service.ts
│   │   └── roles.service.ts
│   └── ventas/
├── middlewares/
│   ├── auth.middleware.ts     # autenticarToken, verificarRol
│   └── validateResource.ts    # Zod validation
├── schemas/
│   ├── usuarios.schema.ts
│   └── ventas.schema.ts
├── v1/
│   └── routes/
│       ├── usuarios/
│       │   └── usuarios.route.ts
│       └── ventas/
├── scripts/
│   └── seed-admin.ts          # Database seeding
└── index.ts                   # App entry point
```

### 10. **Database Patterns**

**Model Associations (Sequelize):**

```typescript
// src/models/usuarios/associations.ts
import Usuario from "./usuarios";
import Rol from "./roles";
import UsuarioRol from "./usuarios_roles";

// Many-to-Many
Usuario.belongsToMany(Rol, {
  through: UsuarioRol,
  foreignKey: "usuario_id",
  otherKey: "rol_id",
  as: "roles"
});

Rol.belongsToMany(Usuario, {
  through: UsuarioRol,
  foreignKey: "rol_id",
  otherKey: "usuario_id",
  as: "usuarios"
});

// One-to-Many for join table
UsuarioRol.belongsTo(Usuario, { foreignKey: "usuario_id", as: "usuario" });
UsuarioRol.belongsTo(Rol, { foreignKey: "rol_id", as: "rol" });
```

**Database Initialization:**

```typescript
// src/index.ts
app.listen(PORT, HOST, async () => {
  try {
    await sequelize.authenticate();
    console.log("Database connected");

    // sync({ force: false }) preserves data
    // sync({ force: true }) drops and recreates (DANGER!)
    await sequelize.sync({ force: false });
    console.log("Models synchronized");
  } catch (error) {
    console.error("Database connection failed:", error);
  }
});
```

## SOLID Principles Application

### **S - Single Responsibility Principle**
- Each layer has ONE responsibility
- Controllers: HTTP handling
- Services: Business logic
- Models: Data structure

### **O - Open/Closed Principle**
- Middleware composition allows extension without modification
- Dynamic includes support new relations without changing core logic

### **L - Liskov Substitution Principle**
- Consistent response format allows interchangeable endpoints
- Middleware can be swapped without breaking the chain

### **I - Interface Segregation Principle**
- Zod schemas define minimal required fields
- Optional `include` parameter avoids forcing unnecessary data

### **D - Dependency Inversion Principle**
- Controllers depend on service abstractions, not concrete implementations
- Services use ORM models (abstraction) rather than direct SQL

## Quick Start Checklist

When creating a new REST API endpoint:

1. ✅ Define Zod schema in `src/schemas/`
2. ✅ Create service function in `src/services/` with business logic
3. ✅ Use transactions for multi-step operations
4. ✅ Create controller in `src/controllers/` that calls service
5. ✅ Return standardized response format
6. ✅ Define route in `src/v1/routes/`
7. ✅ Apply `autenticarToken` middleware if protected
8. ✅ Apply `verificarRol` middleware if role-restricted
9. ✅ Apply `validate(schema)` middleware for input validation
10. ✅ Document in Swagger (`src/config/swagger.ts`)

## Common Patterns

For detailed implementations and code examples, see:
- [Architecture Deep Dive](references/architecture.md) - Detailed layer explanations
- [Transaction Patterns](references/transactions.md) - Complex transaction scenarios
- [Authentication & RBAC](references/auth-rbac.md) - Complete auth implementation
- [Validation Strategies](references/validation.md) - Zod patterns and custom validators
- [Error Handling](references/error-handling.md) - Comprehensive error management
- [Testing Strategies](references/testing.md) - Unit and integration test patterns
- [Performance Optimization](references/performance.md) - Query optimization, caching

## Anti-Patterns to Avoid

❌ **DON'T:**
- Put business logic in controllers
- Make database calls from controllers
- Forget transactions for multi-step operations
- Hardcode role names in code (use database)
- Return raw error messages to clients
- Skip input validation
- Use `sync({ force: true })` in production
- Expose sensitive data in JWT payload
- Use `findAll()` without pagination (for large datasets)

✅ **DO:**
- Keep controllers thin (delegate to services)
- Wrap related operations in transactions
- Validate all input with Zod
- Return standardized response format
- Hash passwords with Argon2
- Cache user roles to avoid repeated DB queries
- Use environment variables for configuration
- Document all endpoints in Swagger

---

**Skill Version:** 1.0.0
**Author:** Miller Marru ([@MILLERMARRU](https://github.com/MILLERMARRU))
**Contact:** millermarru4@gmail.com
**Repository:** https://github.com/MILLERMARRU/express-typescript-api-best-practices
**License:** MIT
**Last Updated:** 2026-02-05

Related Skills

fastapi-best-practices

16
from diegosouzapw/awesome-omni-skill

FastAPI best practices e convenções baseadas em produção real. Aplicar em todos os projetos FastAPI.

express-api-patterns

16
from diegosouzapw/awesome-omni-skill

Express.js API development, route handling, middleware, error handling, request validation, CORS. Use when building Express routes, implementing middleware, handling API requests, or setting up the backend server.

defining-typescript-models

16
from diegosouzapw/awesome-omni-skill

Defines standard TypeScript interfaces for Appwrite Collections. Use when creating new models for Tours, Users, or Bookings to ensure full type safety.

dataverse-python-best-practices

16
from diegosouzapw/awesome-omni-skill

dataverse-python-best-practices guidelines

claude-typescript-sdk

16
from diegosouzapw/awesome-omni-skill

Build AI applications with the Anthropic TypeScript SDK. Use when creating Claude integrations, building agents, implementing tool use, streaming responses, or working with the @anthropic-ai/sdk package.

better-auth-best-practices

16
from diegosouzapw/awesome-omni-skill

Skill for integrating Better Auth - the comprehensive TypeScript authentication framework.

benefriches-react-best-practices

16
from diegosouzapw/awesome-omni-skill

React best practices and performance optimization for Benefriches (Vite + Redux). Reference when writing components, implementing Redux patterns, reviewing code quality, or optimizing performance.

awesome-copilot-root-typescript-mcp-expert

16
from diegosouzapw/awesome-omni-skill

Expert assistant for developing Model Context Protocol (MCP) servers in TypeScript Use when: the task directly matches typescript mcp expert responsibilities within plugin awesome-copilot-root. Do not use when: a more specific framework or task-focused skill is clearly a better match.

auth0-express

16
from diegosouzapw/awesome-omni-skill

Use when adding authentication to Express.js server-rendered web applications with session management - integrates express-openid-connect for traditional web apps

arcanea-react-best-practices

16
from diegosouzapw/awesome-omni-skill

React 19 and Next.js 16 performance optimization for the Arcanea platform. Use when writing, reviewing, or refactoring React components, data fetching logic, bundle optimization, or any frontend performance work. Triggers on: React components, Next.js pages, hooks, data fetching, bundle size, re-renders, Server Components, Client Components, hydration. Sourced from Vercel Engineering's official React best practices (57 rules, 8 categories) and adapted for the Arcanea stack.

apple-dev-best-practices

16
from diegosouzapw/awesome-omni-skill

Apple platform development best practices for Swift 6, SwiftUI, SwiftData, and iOS/macOS apps. Use when building any iOS or macOS app, writing Swift code, designing SwiftUI views, working with Xcode projects, implementing navigation, state management, concurrency, networking, persistence, or testing on Apple platforms. Triggers on Swift, SwiftUI, iOS, macOS, Xcode, UIKit, SwiftData, Core Data, XCTest, StoreKit, CloudKit, MapKit, HealthKit, or any Apple framework. Also use when reviewing Swift code, debugging iOS apps, migrating UIKit to SwiftUI, or planning Apple platform architecture.

api-framework-express

16
from diegosouzapw/awesome-omni-skill

Express.js routes, middleware, error handling, request/response patterns