Skip to main content

Overview

Follow these best practices to build secure, maintainable, and scalable applications with Node Blueprint.

Code Organization

Keep Controllers Thin

Controllers should only handle HTTP concerns (request/response). Move business logic to services. Bad:
// controllers/user-controller.ts
export const createUser = async (req: Request, res: Response) => {
  const { email, password } = req.body;
  
  // Too much logic in controller
  const hashedPassword = await bcrypt.hash(password, 10);
  const user = await db.insert(users).values({ email, password: hashedPassword });
  const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
  
  res.json({ user, token });
};
Good:
// controllers/user-controller.ts
export const createUser = async (req: Request, res: Response) => {
  const { email, password } = req.body;
  const result = await userService.createUser(email, password);
  res.json(result);
};

// services/user-service.ts
export const createUser = async (email: string, password: string) => {
  const hashedPassword = await bcrypt.hash(password, 10);
  const user = await db.insert(users).values({ email, password: hashedPassword });
  const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
  return { user, token };
};
This separation makes your code more testable and reusable across different controllers.

Use Proper Error Handling

Always handle errors gracefully and provide meaningful error messages.
import { Request, Response, NextFunction } from 'express';

export const getUser = async (req: Request, res: Response, next: NextFunction) => {
  try {
    const user = await userService.findById(req.params.id);
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    res.json(user);
  } catch (error) {
    next(error); // Pass to error middleware
  }
};
The generated error middleware in src/middlewares/error-middleware.ts handles errors globally.

Organize by Feature for Large Projects

As your project grows, consider organizing by feature instead of by type:
src/
├── features/
│   ├── users/
│   │   ├── user.controller.ts
│   │   ├── user.service.ts
│   │   ├── user.routes.ts
│   │   ├── user.model.ts
│   │   └── user.validation.ts
│   ├── products/
│   │   ├── product.controller.ts
│   │   ├── product.service.ts
│   │   └── ...
This makes it easier to find related code and enables better code splitting.

Environment Variables

Use Type-Safe Environment Configuration

The generated src/config/env.ts provides type-safe environment variables. Always extend it for new variables:
// src/config/env.ts
import { z } from 'zod';

const envSchema = z.object({
  PORT: z.string().default('8000'),
  NODE_ENV: z.enum(['development', 'production', 'test']),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  // Add your custom variables
  REDIS_URL: z.string().url().optional(),
  STRIPE_SECRET_KEY: z.string().optional(),
});

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

Never Commit Secrets

Always use environment variables for sensitive data:
// Bad
const apiKey = 'sk_live_123456789';

// Good
const apiKey = process.env.STRIPE_SECRET_KEY;
Use .env.example to document required environment variables without exposing actual values.

Use Different Configs per Environment

# .env.development
DATABASE_URL=postgresql://localhost:5432/myapp_dev
LOG_LEVEL=debug

# .env.production
DATABASE_URL=postgresql://prod-db:5432/myapp
LOG_LEVEL=error

Security

Validate All Input

Always validate user input using validation schemas:
import { z } from 'zod';

const createUserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8).max(100),
  name: z.string().min(2).max(50),
});

export const validateCreateUser = (req: Request, res: Response, next: NextFunction) => {
  try {
    createUserSchema.parse(req.body);
    next();
  } catch (error) {
    res.status(400).json({ error: 'Invalid input', details: error });
  }
};

Use Parameterized Queries

All generated ORMs (Drizzle, Prisma, Mongoose) use parameterized queries by default, which prevents SQL injection:
// Safe - Drizzle uses parameterized queries
const user = await db.select().from(users).where(eq(users.email, email));

// Safe - Prisma uses parameterized queries
const user = await prisma.user.findUnique({ where: { email } });

// Never do this (raw SQL without parameters)
const user = await db.execute(`SELECT * FROM users WHERE email = '${email}'`);

Implement Rate Limiting

Protect your API from abuse:
import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests, please try again later',
});

app.use('/api/', limiter);

Use HTTPS in Production

Always use HTTPS in production. The generated Helmet middleware helps with security headers:
// src/middlewares/helmet-middleware.ts is already configured
import helmet from 'helmet';

export const HelmetMiddleware = helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
    },
  },
});
The CORS middleware in src/middlewares/cors-middleware.ts should be configured to only allow trusted origins in production.

Database Best Practices

Use Transactions for Multiple Operations

With Drizzle:
import { db } from '../db/index.js';

await db.transaction(async (tx) => {
  const user = await tx.insert(users).values({ email, password });
  await tx.insert(profiles).values({ userId: user.id, name });
});
With Prisma:
await prisma.$transaction([
  prisma.user.create({ data: { email, password } }),
  prisma.profile.create({ data: { userId, name } }),
]);

Use Database Indexes

Add indexes for frequently queried fields: Drizzle:
import { index } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
}, (table) => {
  return {
    emailIdx: index('email_idx').on(table.email),
  };
});
Prisma:
model User {
  id    Int    @id @default(autoincrement())
  email String @unique
  
  @@index([email])
}

Handle Database Errors

try {
  await userService.createUser(email, password);
} catch (error) {
  if (error.code === '23505') { // Postgres unique constraint
    return res.status(409).json({ error: 'Email already exists' });
  }
  throw error;
}

Performance

Use Database Connection Pooling

The generated database configurations use connection pooling by default:
// Drizzle with connection pooling
import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20, // maximum pool size
});

export const db = drizzle(pool);

Implement Caching

Cache frequently accessed data:
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

export const getUser = async (id: string) => {
  // Check cache first
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);
  
  // Fetch from database
  const user = await db.select().from(users).where(eq(users.id, id));
  
  // Cache for 5 minutes
  await redis.setex(`user:${id}`, 300, JSON.stringify(user));
  
  return user;
};

Use Pagination

Always paginate large datasets:
export const getUsers = async (page = 1, limit = 10) => {
  const offset = (page - 1) * limit;
  
  const users = await db
    .select()
    .from(users)
    .limit(limit)
    .offset(offset);
  
  return users;
};

Testing

Write Tests for Critical Paths

import { describe, it, expect } from 'vitest';
import { createUser } from '../services/user-service.js';

describe('User Service', () => {
  it('should create a user with hashed password', async () => {
    const user = await createUser('[email protected]', 'password123');
    
    expect(user.email).toBe('[email protected]');
    expect(user.password).not.toBe('password123');
  });
  
  it('should throw error for duplicate email', async () => {
    await createUser('[email protected]', 'password123');
    
    await expect(
      createUser('[email protected]', 'password456')
    ).rejects.toThrow();
  });
});

Use Environment-Specific Test Database

// .env.test
DATABASE_URL=postgresql://localhost:5432/myapp_test
Use tools like testcontainers to spin up isolated databases for integration tests.

Logging

Use Structured Logging

The generated Winston logger supports structured logging:
import logger from '../config/logger.js';

logger.info('User created', {
  userId: user.id,
  email: user.email,
  timestamp: new Date().toISOString(),
});

logger.error('Failed to process payment', {
  error: error.message,
  userId: user.id,
  amount: payment.amount,
});

Log Levels

Use appropriate log levels:
  • error - Critical errors that need immediate attention
  • warn - Warning messages for potentially harmful situations
  • info - Informational messages about application flow
  • debug - Detailed debugging information (development only)
if (process.env.NODE_ENV === 'development') {
  logger.debug('Database query', { sql, params });
}

Deployment

Use Process Managers

Use PM2 or similar for production:
# Install PM2
npm install -g pm2

# Start application
pm2 start dist/server.js --name "my-app"

# Auto-restart on file changes
pm2 start dist/server.js --watch

# Save configuration
pm2 save

# Setup startup script
pm2 startup

Health Checks

The generated health endpoint is useful for monitoring:
// Use in Kubernetes liveness probe
livenessProbe:
  httpGet:
    path: /api/health
    port: 8000
  initialDelaySeconds: 30
  periodSeconds: 10

Graceful Shutdown

The generated src/server.ts includes graceful shutdown handling:
const shutdown = () => {
  server.close(() => {
    logger.info('Shutting down server...');
    // Close database connections
    db.end();
    process.exit(0);
  });
};

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
This ensures in-flight requests complete before the server shuts down.

Documentation

Document Your API

Consider adding API documentation:
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';

const specs = swaggerJsdoc({
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'My API',
      version: '1.0.0',
    },
  },
  apis: ['./src/routes/*.ts'],
});

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));

Add Code Comments

Comment complex logic and business rules:
/**
 * Calculates the discounted price based on user tier
 * 
 * @param price - Original price
 * @param userTier - User's subscription tier
 * @returns Discounted price
 */
export const calculateDiscount = (price: number, userTier: string): number => {
  // Premium users get 20% off
  if (userTier === 'premium') return price * 0.8;
  
  // Basic users get 10% off
  if (userTier === 'basic') return price * 0.9;
  
  return price;
};

Monitoring

Add Application Monitoring

Consider using APM tools:
// Using New Relic
import newrelic from 'newrelic';

// Using Sentry
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
});

app.use(Sentry.Handlers.errorHandler());

Track Key Metrics

  • Response times
  • Error rates
  • Database query performance
  • Memory usage
  • CPU usage

Next Steps

Build docs developers (and LLMs) love