Skip to content
Back to skills

Afrexai Nodejs Production

ASecurity

Complete methodology for building production-grade Node.js backends with TypeScript. Covers architecture, frameworks, error handling, database patterns, security, testing, observability, and deployment. ---

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 18, 2026
devopstypescriptgosqlnodeexpressdockertestinggitapidatabase

Works with

  • cursor
  • cli
  • api

Security analysis

A100/100

Scanned October 18, 2026

npx -y skills add johnalbertini14-glitch/openclaw-skills --skill afrexai-nodejs-production --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Afrexai Nodejs Production?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Afrexai Nodejs Production
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/johnalbertini14-glitch-afrexai-nodejs-production/badge)](https://www.skillsdirectory.com/skills/johnalbertini14-glitch-afrexai-nodejs-production)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# Node.js & TypeScript Production Engineering

Complete methodology for building production-grade Node.js backends with TypeScript. Covers architecture, frameworks, error handling, database patterns, security, testing, observability, and deployment.

---

## Quick Health Check (/16)

Run through these 8 signals — score 0 (missing) or 2 (present):

| # | Signal | Check |
|---|--------|-------|
| 1 | Strict TypeScript | `"strict": true` in tsconfig, no `any` escape hatches |
| 2 | Structured errors | Custom error classes with codes, not string throws |
| 3 | Input validation | Zod/Valibot on every external boundary |
| 4 | Database migrations | Version-controlled, reversible, CI-enforced |
| 5 | Health endpoints | `/health` (liveness) + `/ready` (readiness) with dependency checks |
| 6 | Structured logging | JSON logs with request IDs, no `console.log` in prod |
| 7 | Test coverage | >80% unit, integration tests for critical paths |
| 8 | Graceful shutdown | SIGTERM handler drains connections before exit |

**Score interpretation:** 0-6 = critical gaps, 8-10 = needs work, 12-14 = solid, 16 = production-grade.

---

## Phase 1: Architecture & Project Structure

### Framework Selection Matrix

| Framework | Best For | Throughput | Ecosystem | Learning Curve | TypeScript |
|-----------|----------|-----------|-----------|----------------|------------|
| **Hono** | Edge/serverless, lightweight APIs | ⭐⭐⭐⭐⭐ | Growing fast | Low | Native |
| **Fastify** | High-perf monoliths, JSON APIs | ⭐⭐⭐⭐⭐ | Mature | Medium | Excellent |
| **Express** | Legacy, max middleware ecosystem | ⭐⭐⭐ | Massive | Low | Via @types |
| **NestJS** | Enterprise, large teams, DI-heavy | ⭐⭐⭐⭐ | Large | High | Native |
| **Elysia** | Bun-first, type-safe APIs | ⭐⭐⭐⭐⭐ | Small | Low | Native |
| **tRPC** | Full-stack TS, type-safe RPC | N/A (layer) | Growing | Medium | Native |

**Decision rules:**
- New project, small team → **Hono** (portable, fast, minimal)
- High throughput JSON API → **Fastify** (proven, benchmarked)
- Enterprise, 10+ developers → **NestJS** (structure enforced)
- Full-stack TypeScript monorepo → **tRPC** (end-to-end types)
- Existing Express codebase → Stay on Express, migrate incrementally
- Bun runtime → **Elysia** or **Hono**

### Recommended Project Structure

```
src/
├── index.ts              # Entry point — bootstrap only
├── app.ts                # App factory (createApp)
├── config/
│   ├── env.ts            # Environment validation (Zod)
│   └── database.ts       # DB connection config
├── routes/
│   ├── index.ts          # Route registry
│   ├── users.ts          # /users routes
│   └── orders.ts         # /orders routes
├── services/
│   ├── user.service.ts   # Business logic
│   └── order.service.ts
├── repositories/
│   ├── user.repo.ts      # Data access (Drizzle/Prisma)
│   └── order.repo.ts
├── middleware/
│   ├── auth.ts           # Authentication
│   ├── validate.ts       # Request validation
│   ├── error-handler.ts  # Global error handler
│   └── request-id.ts     # Correlation ID
├── errors/
│   └── index.ts          # Custom error classes
├── types/
│   └── index.ts          # Shared types
├── utils/
│   └── index.ts          # Pure utility functions
├── jobs/                 # Background jobs/queues
│   └── email.job.ts
└── __tests__/            # Tests mirror src/ structure
    ├── services/
    └── routes/
drizzle/                  # Database migrations
├── 0001_create_users.sql
└── meta/
tsconfig.json
package.json
Dockerfile
docker-compose.yml
.env.example
```

### 7 Architecture Rules

1. **Routes → Services → Repositories** — never skip layers
2. **Services contain business logic** — routes are thin (validate + call service + respond)
3. **Repositories own data access** — services never import DB client directly
4. **No circular dependencies** — dependency flow is strictly downward
5. **One export per file** — makes imports predictable, testing easy
6. **Config validated at startup** — fail fast, not at runtime
7. **≤50 lines per function, ≤300 lines per file** — split when exceeded

---

## Phase 2: TypeScript Configuration

### Production tsconfig.json

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "exactOptionalPropertyTypes": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
```

### 8 TypeScript Rules

1. **Never use `any`** — use `unknown` and narrow, or define proper types
2. **`noUncheckedIndexedAccess: true`** — forces null checks on array/object access
3. **Branded types for IDs** — `type UserId = string & { __brand: 'UserId' }`
4. **Zod schemas derive types** — `type User = z.infer<typeof UserSchema>`
5. **Discriminated unions for states** — `{ status: 'active'; data: T } | { status: 'error'; error: E }`
6. **`satisfies` over `as`** — preserves narrowed types: `config satisfies Config`
7. **Enums → const objects** — `const Status = { ACTIVE: 'active', INACTIVE: 'inactive' } as const`
8. **Return types on public APIs** — explicit return types on exported functions

### Branded Types Pattern

```typescript
// types/branded.ts
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { [__brand]: B };

export type UserId = Brand<string, 'UserId'>;
export type OrderId = Brand<string, 'OrderId'>;
export type Email = Brand<string, 'Email'>;

export function UserId(id: string): UserId { return id as UserId; }
export function Email(email: string): Email {
  if (!email.includes('@')) throw new ValidationError('Invalid email');
  return email as Email;
}
```

---

## Phase 3: Environment & Configuration

### Validated Configuration with Zod

```typescript
// config/env.ts
import { z } from 'zod';

const EnvSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),
  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url().optional(),
  JWT_SECRET: z.string().min(32),
  JWT_EXPIRES_IN: z.string().default('15m'),
  LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
  CORS_ORIGINS: z.string().transform(s => s.split(',')).default('*'),
  RATE_LIMIT_MAX: z.coerce.number().default(100),
  RATE_LIMIT_WINDOW_MS: z.coerce.number().default(60_000),
});

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

// Validate at import time — fail fast
const parsed = EnvSchema.safeParse(process.env);
if (!parsed.success) {
  console.error('❌ Invalid environment variables:', parsed.error.flatten().fieldErrors);
  process.exit(1);
}

export const env = parsed.data;
```

### 5 Configuration Rules

1. **Validate ALL env vars at startup** — never read `process.env` directly elsewhere
2. **Type-safe access only** — import `env` object, not `process.env`
3. **Secrets via vault/env** — never hardcoded, never committed
4. **`.env.example` always current** — document every var with description + default
5. **Different configs per environment** — use `NODE_ENV` for branching, never feature flags in env

---

## Phase 4: Error Handling Architecture

### Custom Error Hierarchy

```typescript
// errors/index.ts
export class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number,
    public readonly details?: Record<string, unknown>,
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

export class ValidationError extends AppError {
  constructor(message: string, details?: Record<string, unknown>) {
    super(message, 'VALIDATION_ERROR', 400, details);
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} not found: ${id}`, 'NOT_FOUND', 404, { resource, id });
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = 'Authentication required') {
    super(message, 'UNAUTHORIZED', 401);
  }
}

export class ForbiddenError extends AppError {
  constructor(message = 'Insufficient permissions') {
    super(message, 'FORBIDDEN', 403);
  }
}

export class ConflictError extends AppError {
  constructor(message: string, details?: Record<string, unknown>) {
    super(message, 'CONFLICT', 409, details);
  }
}

export class RateLimitError extends AppError {
  constructor(retryAfterMs: number) {
    super('Too many requests', 'RATE_LIMITED', 429, { retryAfterMs });
  }
}
```

### Global Error Handler

```typescript
// middleware/error-handler.ts
import type { ErrorHandler } from 'hono';
import { AppError } from '../errors/index.js';
import { logger } from '../utils/logger.js';

export const errorHandler: ErrorHandler = (err, c) => {
  const requestId = c.get('requestId');

  if (err instanceof AppError) {
    if (err.statusCode >= 500) {
      logger.error({ err, requestId }, 'Server error');
    } else {
      logger.warn({ err, requestId }, 'Client error');
    }

    return c.json({
      error: { code: err.code, message: err.message, details: err.details },
    }, err.statusCode as any);
  }

  // Unexpected errors — don't leak internals
  logger.error({ err, requestId }, 'Unhandled error');
  return c.json({
    error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' },
  }, 500);
};
```

### 6 Error Handling Rules

1. **Throw custom errors, catch at boundary** — services throw, routes/middleware catch
2. **Never throw strings** — always `throw new SomeError(message)`
3. **Don't leak internals** — 5xx returns generic message, log the real error
4. **Include error codes** — machine-readable codes for client handling
5. **Validate early, fail fast** — Zod at route level before business logic
6. **Async errors auto-caught** — use frameworks that handle rejected promises (Hono/Fastify do this natively)

---

## Phase 5: Input Validation

### Zod Schema Patterns

```typescript
// routes/users.ts
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';

const CreateUserSchema = z.object({
  email: z.string().email().max(255).toLowerCase(),
  name: z.string().min(1).max(100).trim(),
  role: z.enum(['user', 'admin']).default('user'),
});

const PaginationSchema = z.object({
  cursor: z.string().optional(),
  limit: z.coerce.number().int().min(1).max(100).default(20),
});

const UserIdParamSchema = z.object({
  id: z.string().uuid(),
});

// Usage with Hono
app.post('/users', zValidator('json', CreateUserSchema), async (c) => {
  const body = c.req.valid('json'); // Fully typed!
  const user = await userService.create(body);
  return c.json({ data: user }, 201);
});

app.get('/users', zValidator('query', PaginationSchema), async (c) => {
  const { cursor, limit } = c.req.valid('query');
  const result = await userService.list({ cursor, limit });
  return c.json({ data: result.items, nextCursor: result.nextCursor });
});
```

### Validation Rules

1. **Validate at the edge** — every route handler validates input before calling services
2. **Schemas define the contract** — derive TypeScript types from Zod, not the other way around
3. **Transform in schemas** — `.trim()`, `.toLowerCase()`, `.default()` in Zod, not in services
4. **Reuse common schemas** — `PaginationSchema`, `DateRangeSchema`, `SortSchema`
5. **Validate path params too** — UUIDs, slugs, numeric IDs

---

## Phase 6: Database Patterns

### ORM Selection Guide

| ORM | Best For | Type Safety | Migration | Query Builder | Learning Curve |
|-----|----------|-------------|-----------|---------------|----------------|
| **Drizzle** | SQL-first, edge, performance | ⭐⭐⭐⭐⭐ | Built-in | SQL-like | Low |
| **Prisma** | Rapid dev, schema-first | ⭐⭐⭐⭐ | Built-in | Custom DSL | Low |
| **Kysely** | SQL purists, complex queries | ⭐⭐⭐⭐⭐ | External | Type-safe SQL | Medium |
| **TypeORM** | Legacy, decorator-style | ⭐⭐⭐ | Built-in | ORM-style | Medium |

**Decision:** Drizzle for new projects (matches SQL mental model, best TypeScript inference, edge-compatible).

### Drizzle Schema Example

```typescript
// drizzle/schema.ts
import { pgTable, text, timestamp, uuid, varchar, boolean, index } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 100 }).notNull(),
  role: text('role', { enum: ['user', 'admin'] }).notNull().default('user'),
  isActive: boolean('is_active').notNull().default(true),
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
  index('idx_users_email').on(table.email),
  index('idx_users_created').on(table.createdAt),
]);
```

### Repository Pattern

```typescript
// repositories/user.repo.ts
import { eq, desc, gt } from 'drizzle-orm';
import { db } from '../config/database.js';
import { users } from '../../drizzle/schema.js';
import type { UserId } from '../types/branded.js';
import { NotFoundError } from '../errors/index.js';

export class UserRepository {
  async findById(id: UserId) {
    const [user] = await db.select().from(users).where(eq(users.id, id)).limit(1);
    if (!user) throw new NotFoundError('User', id);
    return user;
  }

  async list(opts: { cursor?: string; limit: number }) {
    const query = db.select().from(users).orderBy(desc(users.createdAt)).limit(opts.limit + 1);
    if (opts.cursor) query.where(gt(users.id, opts.cursor));
    const rows = await query;
    const hasMore = rows.length > opts.limit;
    return { items: rows.slice(0, opts.limit), nextCursor: hasMore ? rows[opts.limit - 1].id : undefined };
  }

  async create(data: { email: string; name: string; role?: string }) {
    const [user] = await db.insert(users).values(data).returning();
    return user;
  }

  async update(id: UserId, data: Partial<{ name: string; role: string; isActive: boolean }>) {
    const [user] = await db.update(users).set({ ...data, updatedAt: new Date() }).where(eq(users.id, id)).returning();
    if (!user) throw new NotFoundError('User', id);
    return user;
  }
}
```

### Database Rules

1. **Cursor-based pagination** — never OFFSET for user-facing lists
2. **Migrations in version control** — `drizzle-kit generate` → commit → `drizzle-kit migrate` in CI
3. **Connection pooling** — use `pg` pool (min: 2, max: 10 per process) or serverless driver for edge
4. **Transactions for multi-write** — `db.transaction(async (tx) => { ... })`
5. **Index query patterns** — add indexes for WHERE + ORDER BY columns, monitor slow queries
6. **SQLite for dev, Postgres for prod** — Drizzle supports both with same schema (use `better-sqlite3` locally)

---

## Phase 7: Authentication & Authorization

### JWT Auth Middleware (Hono)

```typescript
// middleware/auth.ts
import { jwt } from 'hono/jwt';
import { env } from '../config/env.js';
import type { UserId } from '../types/branded.js';

type JwtPayload = { sub: UserId; role: 'user' | 'admin'; iat: number; exp: number };

export const authenticate = jwt({ secret: env.JWT_SECRET });

export const requireRole = (...roles: string[]) => {
  return async (c: any, next: any) => {
    const payload = c.get('jwtPayload') as JwtPayload;
    if (!roles.includes(payload.role)) {
      throw new ForbiddenError(`Required role: ${roles.join(' or ')}`);
    }
    await next();
  };
};
```

### Security Checklist

| # | Item | Priority |
|---|------|----------|
| 1 | Helmet/security headers (CSP, HSTS, X-Frame) | P0 |
| 2 | Rate limiting per IP + per user | P0 |
| 3 | Input validation on every endpoint | P0 |
| 4 | CORS configured (not `*` in prod) | P0 |
| 5 | JWT short-lived (15m) + refresh token rotation | P0 |
| 6 | Password hashing (argon2id, cost ≥ 3) | P0 |
| 7 | SQL injection prevention (parameterized queries) | P0 |
| 8 | Request size limits (1MB default) | P1 |
| 9 | Dependency audit (`npm audit`, Snyk) | P1 |
| 10 | API key scoping (read-only, write, admin) | P1 |

---

## Phase 8: Structured Logging & Observability

### Pino Logger Setup

```typescript
// utils/logger.ts
import pino from 'pino';
import { env } from '../config/env.js';

export const logger = pino({
  level: env.LOG_LEVEL,
  ...(env.NODE_ENV === 'development' && { transport: { target: 'pino-pretty' } }),
  serializers: { err: pino.stdSerializers.err },
  redact: ['req.headers.authorization', '*.password', '*.token', '*.secret'],
  formatters: {
    level: (label) => ({ level: label }),
  },
});
```

### Request ID Middleware

```typescript
// middleware/request-id.ts
import { randomUUID } from 'node:crypto';

export const requestId = () => {
  return async (c: any, next: any) => {
    const id = c.req.header('x-request-id') || randomUUID();
    c.set('requestId', id);
    c.header('x-request-id', id);
    await next();
  };
};
```

### Request Logging Middleware

```typescript
// middleware/request-logger.ts
import { logger } from '../utils/logger.js';

export const requestLogger = () => {
  return async (c: any, next: any) => {
    const start = performance.now();
    const requestId = c.get('requestId');

    await next();

    const duration = Math.round(performance.now() - start);
    const level = c.res.status >= 500 ? 'error' : c.res.status >= 400 ? 'warn' : 'info';

    logger[level]({
      requestId,
      method: c.req.method,
      path: c.req.path,
      status: c.res.status,
      durationMs: duration,
      userAgent: c.req.header('user-agent'),
    }, `${c.req.method} ${c.req.path} ${c.res.status} ${duration}ms`);
  };
};
```

### Health Endpoints

```typescript
// routes/health.ts
app.get('/health', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() }));

app.get('/ready', async (c) => {
  const checks = {
    database: false,
    redis: false,
  };

  try {
    await db.execute(sql`SELECT 1`);
    checks.database = true;
  } catch {}

  try {
    await redis.ping();
    checks.redis = true;
  } catch {}

  const ready = Object.values(checks).every(Boolean);
  return c.json({ status: ready ? 'ready' : 'degraded', checks }, ready ? 200 : 503);
});
```

---

## Phase 9: Testing Strategy

### Test Pyramid

| Level | Coverage Target | Tools | What to Test |
|-------|----------------|-------|-------------|
| **Unit** | >80% | Vitest | Services, utils, pure logic |
| **Integration** | Critical paths | Vitest + testcontainers | Routes → DB round-trip |
| **E2E** | Happy paths | Vitest + supertest | Full HTTP request cycle |
| **Contract** | API boundaries | Vitest | Request/response shapes |

### Vitest Configuration

```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['src/**/*.test.ts'],
    coverage: { provider: 'v8', reporter: ['text', 'lcov'], thresholds: { lines: 80, branches: 75 } },
    setupFiles: ['./src/__tests__/setup.ts'],
    pool: 'forks', // Isolation for DB tests
  },
});
```

### Service Unit Test Pattern

```typescript
// __tests__/services/user.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from '../../services/user.service.js';
import type { UserRepository } from '../../repositories/user.repo.js';

describe('UserService', () => {
  let service: UserService;
  let repo: jest.Mocked<UserRepository>;

  beforeEach(() => {
    repo = { findById: vi.fn(), create: vi.fn(), update: vi.fn(), list: vi.fn() } as any;
    service = new UserService(repo);
  });

  it('creates user with normalized email', async () => {
    repo.create.mockResolvedValue({ id: '1', email: 'test@example.com', name: 'Test' } as any);
    const result = await service.create({ email: 'Test@Example.COM', name: 'Test' });
    expect(repo.create).toHaveBeenCalledWith(expect.objectContaining({ email: 'test@example.com' }));
    expect(result.email).toBe('test@example.com');
  });
});
```

### Integration Test Pattern

```typescript
// __tests__/routes/users.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createApp } from '../../app.js';
import { testDb, migrate, cleanup } from '../helpers/db.js';

describe('POST /users', () => {
  let app: any;

  beforeAll(async () => {
    await migrate(testDb);
    app = createApp({ db: testDb });
  });

  afterAll(async () => { await cleanup(testDb); });

  it('creates user and returns 201', async () => {
    const res = await app.request('/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: 'new@example.com', name: 'New User' }),
    });

    expect(res.status).toBe(201);
    const body = await res.json();
    expect(body.data.email).toBe('new@example.com');
  });

  it('rejects invalid email with 400', async () => {
    const res = await app.request('/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: 'not-an-email', name: 'Bad' }),
    });

    expect(res.status).toBe(400);
  });
});
```

### 7 Testing Rules

1. **Test behavior, not implementation** — assert outputs, not internal method calls
2. **Each test is independent** — no shared mutable state between tests
3. **Name tests as specifications** — `it('rejects expired JWT with 401')`
4. **Fast unit tests, isolated integration tests** — use `pool: 'forks'` for DB tests
5. **Mock at boundaries** — mock repositories in service tests, real DB in integration tests
6. **Test error paths** — 400s, 404s, 409s, 500s, not just happy paths
7. **CI enforces coverage** — fail build if coverage drops below threshold

---

## Phase 10: Graceful Shutdown & Process Management

### Shutdown Handler

```typescript
// index.ts
import { serve } from '@hono/node-server';
import { createApp } from './app.js';
import { logger } from './utils/logger.js';
import { env } from './config/env.js';
import { db } from './config/database.js';

const app = createApp();
const server = serve({ fetch: app.fetch, port: env.PORT });

logger.info({ port: env.PORT }, 'Server started');

const shutdown = async (signal: string) => {
  logger.info({ signal }, 'Shutdown signal received');

  // Stop accepting new connections
  server.close(() => { logger.info('HTTP server closed'); });

  // Drain existing work (give 10s)
  const timeout = setTimeout(() => {
    logger.error('Forced shutdown after timeout');
    process.exit(1);
  }, 10_000);

  try {
    // Close DB pool, Redis, queues, etc.
    await db.$client.end();
    logger.info('Database pool closed');
    clearTimeout(timeout);
    process.exit(0);
  } catch (err) {
    logger.error({ err }, 'Error during shutdown');
    process.exit(1);
  }
};

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

// Catch unhandled errors
process.on('unhandledRejection', (err) => {
  logger.fatal({ err }, 'Unhandled rejection');
  process.exit(1);
});
```

---

## Phase 11: Production Deployment

### Multi-Stage Dockerfile

```dockerfile
# Build stage
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --ignore-scripts
COPY tsconfig.json ./
COPY src/ src/
COPY drizzle/ drizzle/
RUN npm run build
RUN npm ci --omit=dev --ignore-scripts

# Production stage
FROM node:22-alpine
RUN apk add --no-cache tini dumb-init
WORKDIR /app
COPY --from=builder /app/dist dist/
COPY --from=builder /app/drizzle drizzle/
COPY --from=builder /app/node_modules node_modules/
COPY --from=builder /app/package.json .

ENV NODE_ENV=production
USER node
EXPOSE 3000
ENTRYPOINT ["tini", "--"]
CMD ["node", "dist/index.js"]
```

### GitHub Actions CI

```yaml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_DB: test, POSTGRES_PASSWORD: test }
        ports: ['5432:5432']
        options: >-
          --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test -- --coverage
        env: { DATABASE_URL: 'postgresql://postgres:test@localhost:5432/test' }
      - run: npm run build
```

### Production Checklist

**P0 — Mandatory:**
- [ ] `NODE_ENV=production`
- [ ] Strict TypeScript (`tsc --noEmit` passes)
- [ ] All env vars validated at startup
- [ ] Health + readiness endpoints
- [ ] Graceful shutdown handles SIGTERM
- [ ] Structured JSON logging (no console.log)
- [ ] Error handler doesn't leak stack traces
- [ ] Rate limiting enabled
- [ ] CORS configured (not wildcard)
- [ ] Security headers set

**P1 — Recommended:**
- [ ] Request ID tracing through all logs
- [ ] Database connection pooling configured
- [ ] Dependency audit clean (`npm audit`)
- [ ] Docker image < 200MB
- [ ] Response compression (gzip/brotli)
- [ ] API versioning strategy decided
- [ ] Monitoring/alerting configured

---

## Phase 12: Performance Optimization

### Priority Stack

| # | Technique | Impact | Effort |
|---|-----------|--------|--------|
| 1 | Connection pooling (DB + Redis) | High | Low |
| 2 | Response caching (Redis/in-memory) | High | Medium |
| 3 | Query optimization (indexes, N+1) | High | Medium |
| 4 | JSON serialization (fast-json-stringify) | Medium | Low |
| 5 | Compression middleware | Medium | Low |
| 6 | Streaming responses for large payloads | Medium | Medium |
| 7 | Worker threads for CPU-bound work | High | High |
| 8 | HTTP/2 + keep-alive | Low | Low |

### Common Optimizations

```typescript
// N+1 prevention — batch with dataloader or SQL JOIN
// ❌ Bad: for loop with individual queries
for (const order of orders) {
  order.user = await db.query.users.findFirst({ where: eq(users.id, order.userId) });
}

// ✅ Good: single query with join
const ordersWithUsers = await db
  .select()
  .from(orders)
  .leftJoin(users, eq(orders.userId, users.id))
  .where(inArray(orders.id, orderIds));

// In-memory caching for hot data
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, any>({ max: 1000, ttl: 60_000 });

async function getCachedUser(id: string) {
  const cached = cache.get(id);
  if (cached) return cached;
  const user = await userRepo.findById(id);
  cache.set(id, user);
  return user;
}
```

---

## Phase 13: Background Jobs & Queues

### BullMQ Pattern

```typescript
// jobs/email.job.ts
import { Queue, Worker } from 'bullmq';
import { env } from '../config/env.js';
import { logger } from '../utils/logger.js';

const connection = { url: env.REDIS_URL };

export const emailQueue = new Queue('email', { connection });

export const emailWorker = new Worker('email', async (job) => {
  const { to, subject, body } = job.data;
  logger.info({ jobId: job.id, to }, 'Sending email');
  // Send via provider
  await sendEmail({ to, subject, body });
}, {
  connection,
  concurrency: 5,
  limiter: { max: 10, duration: 1000 }, // 10 per second
});

emailWorker.on('failed', (job, err) => {
  logger.error({ jobId: job?.id, err }, 'Email job failed');
});
```

### Job Rules

1. **Idempotent jobs** — same job ID running twice produces same result
2. **Retry with backoff** — exponential backoff for transient failures
3. **Dead letter queue** — failed jobs after max retries go to DLQ for inspection
4. **Job timeout** — set `removeOnComplete` and `removeOnFail` TTLs
5. **Monitor queue health** — queue length, processing time, failure rate

---

## Phase 14: Advanced Patterns

### Dependency Injection (Manual)

```typescript
// app.ts — compose dependencies explicitly
export function createApp(deps?: { db?: Database; redis?: Redis }) {
  const database = deps?.db ?? defaultDb;
  const app = new Hono();

  // Compose service graph
  const userRepo = new UserRepository(database);
  const userService = new UserService(userRepo);

  // Mount routes with injected services
  app.route('/users', createUserRoutes(userService));

  return app;
}
```

### Rate Limiting

```typescript
// middleware/rate-limit.ts (using hono-rate-limiter)
import { rateLimiter } from 'hono-rate-limiter';

export const apiRateLimit = rateLimiter({
  windowMs: 60_000,
  limit: 100,
  keyGenerator: (c) => c.get('jwtPayload')?.sub || c.req.header('x-forwarded-for') || 'anonymous',
  message: { error: { code: 'RATE_LIMITED', message: 'Too many requests' } },
});
```

### WebSocket Pattern (Hono + @hono/node-ws)

```typescript
import { createNodeWebSocket } from '@hono/node-ws';

const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });

app.get('/ws', upgradeWebSocket((c) => ({
  onOpen(evt, ws) { logger.info('WebSocket connected'); },
  onMessage(evt, ws) {
    const data = JSON.parse(evt.data.toString());
    // Handle message
    ws.send(JSON.stringify({ type: 'ack', id: data.id }));
  },
  onClose() { logger.info('WebSocket disconnected'); },
})));
```

---

## 2025+ Recommended Stack

| Layer | Recommended | Alternative |
|-------|-------------|-------------|
| **Runtime** | Node.js 22 LTS | Bun |
| **Framework** | Hono | Fastify |
| **Language** | TypeScript (strict) | — |
| **Validation** | Zod | Valibot |
| **ORM** | Drizzle | Prisma |
| **Database** | PostgreSQL | SQLite (dev) |
| **Cache** | Redis / Upstash | LRU in-memory |
| **Auth** | JWT + refresh | Lucia, Better Auth |
| **Queue** | BullMQ | pg-boss |
| **Testing** | Vitest | — |
| **Logging** | Pino | — |
| **Linting** | Biome | ESLint + Prettier |
| **CI/CD** | GitHub Actions | — |
| **Deploy** | Docker + Railway/Fly | Vercel (serverless) |

---

## 10 Commandments

1. **Validate everything at the boundary** — Zod on every route
2. **Fail fast at startup** — env vars, DB connection, migrations
3. **Structured logging everywhere** — JSON, request IDs, no console.log
4. **Custom errors with codes** — never throw strings
5. **Cursor pagination** — never OFFSET
6. **Graceful shutdown** — drain connections on SIGTERM
7. **Test error paths** — 4xx/5xx matter more than happy paths
8. **Type everything** — no `any`, branded IDs, Zod-derived types
9. **Security by default** — rate limit, helmet, CORS, audit deps
10. **Keep it simple** — 50 lines/function, 300 lines/file, flat is better than nested

---

## 10 Common Mistakes

| # | Mistake | Fix |
|---|---------|-----|
| 1 | `console.log` in production | Use Pino with JSON output |
| 2 | `any` type everywhere | `unknown` + type narrowing |
| 3 | No input validation | Zod middleware on every route |
| 4 | OFFSET pagination | Cursor-based with keyset |
| 5 | No graceful shutdown | SIGTERM handler with drain timeout |
| 6 | Hardcoded config | Zod-validated env at startup |
| 7 | Fat controllers | Thin routes → services → repositories |
| 8 | No error hierarchy | Custom AppError with codes |
| 9 | Testing only happy paths | Test 400/401/404/409/500 explicitly |
| 10 | No request tracing | Request ID middleware + propagation |

---

## Quality Scoring (0-100)

| Dimension | Weight | What to Assess |
|-----------|--------|----------------|
| Type safety | 20% | Strict TS, no `any`, branded types |
| Error handling | 15% | Custom errors, global handler, no leaks |
| Testing | 15% | Coverage >80%, integration tests, error paths |
| Security | 15% | Auth, validation, rate limiting, headers |
| Observability | 10% | Structured logging, health checks, metrics |
| Performance | 10% | Connection pooling, caching, N+1 prevention |
| Code structure | 10% | Layer separation, file size limits, DI |
| Deployment | 5% | Docker, CI/CD, graceful shutdown |

---

## Edge Cases

### Startup/MVP
- Start with Hono + SQLite (Drizzle) + Vitest — ship in hours
- Add Postgres when you need concurrent writes
- Skip Redis until you need caching or queues

### Monorepo (Turborepo/Nx)
- Shared packages: `@org/types`, `@org/db`, `@org/validation`
- Each service is its own package with independent build/test
- Use workspace protocol: `"@org/types": "workspace:*"`

### Serverless (Vercel/Cloudflare Workers)
- Hono runs everywhere — same code, different adapters
- Use serverless DB drivers (Neon, PlanetScale, Turso)
- No graceful shutdown needed — stateless by design

### Legacy Express Migration
- Add TypeScript incrementally (`.ts` files alongside `.js`)
- Wrap Express routes with Zod validation middleware
- Replace `console.log` with Pino one file at a time
- Migrate to Hono/Fastify route-by-route using adapter

---

## Natural Language Commands

- "Set up a new TypeScript API project" → Phase 1-3 (structure + config + env)
- "Add authentication to my API" → Phase 7 (JWT + RBAC)
- "Review my error handling" → Phase 4 (error hierarchy + handler)
- "Add database with Drizzle" → Phase 6 (schema + repository + migrations)
- "Set up testing" → Phase 9 (Vitest + unit + integration patterns)
- "Prepare for production deployment" → Phase 11 (Docker + CI + checklist)
- "Optimize API performance" → Phase 12 (priority stack + patterns)
- "Add background jobs" → Phase 13 (BullMQ + patterns)
- "Audit my API security" → Phase 7 security checklist
- "Set up logging and monitoring" → Phase 8 (Pino + request ID + health)
- "Review my project structure" → Phase 1 (structure + rules)
- "Full health check" → Quick Health Check + Quality Scoring

---

⚡ Built by [AfrexAI](https://afrexai-cto.github.io/context-packs/) — AI-powered business automation.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.