Research-mind toolchains-typescript-frameworks-nodejs-backend
Node.js Backend Development with TypeScript
install
source · Clone the upstream repo
git clone https://github.com/MacPhobos/research-mind
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/MacPhobos/research-mind "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/toolchains-typescript-frameworks-nodejs-backend" ~/.claude/skills/macphobos-research-mind-toolchains-typescript-frameworks-nodejs-backend && rm -rf "$T"
manifest:
.claude/skills/toolchains-typescript-frameworks-nodejs-backend/skill.mdsource content
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' && { stack: err.stack }), }); } console.error('Unexpected error:', err); res.status(500).json({ error: 'Internal server error', ...(process.env.NODE_ENV === 'development' && { 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 && eq(users.status, filters.status), filters.search && 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 && { 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