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
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;
}
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;
};
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));
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