blog.dopana

Back

Node.js chạy hơn 50% ứng dụng web backend. Nhưng dễ viết, khó làm đúng. Bài này tổng hợp những best practices thực tế.

1. Cấu Trúc Project — Folder-by-Feature#

Folder-by-layer (controllers/, services/, models/) lộn xộn khi project lớn. Folder-by-feature — mỗi module tự quản lý route, controller, service, test riêng.

2. Error Handling — Không Để Crash#

Custom Error Class#

export class AppError extends Error {
  constructor(
    public statusCode: number,
    public message: string,
    public code?: string
  ) {
    super(message);
    this.name = 'AppError';
  }
}

// Usage
throw new AppError(404, 'User not found', 'USER_NOT_FOUND');
typescript

Global Error Handler#

app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: { code: err.code, message: err.message }
    });
  }

  // Lỗi không mong đợi — log và trả về 500
  console.error('Unexpected error:', err);
  res.status(500).json({
    error: { message: 'Internal server error' }
  });
});
typescript

3. Async — Luôn Dùng try/catch Hoặc Promise.catch#

// ❌ Tệ — crash nếu reject
app.get('/users/:id', async (req, res) => {
  const user = await userService.findById(req.params.id);
  res.json(user);
});

// ✅ Tốt — wrapper
const asyncHandler = (fn: Function) =>
  (req: Request, res: Response, next: NextFunction) =>
    Promise.resolve(fn(req, res, next)).catch(next);

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await userService.findById(req.params.id);
  res.json(user);
}));
typescript

4. Environment Variables — Zod Validation#

import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['dev', 'prod', 'test']).default('dev'),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  REDIS_URL: z.string().url().optional(),
});

export const env = envSchema.parse(process.env);
// Throw ngay khi start nếu thiếu biến — không đợi runtime
typescript

5. Logging — Có Cấu Trúc#

import pino from 'pino';

const logger = pino({
  level: env.NODE_ENV === 'prod' ? 'info' : 'debug',
  transport: env.NODE_ENV === 'dev'
    ? { target: 'pino-pretty' }
    : undefined,
});

logger.info({ userId, action: 'login' }, 'User logged in');
logger.error({ err, orderId }, 'Failed to process order');
typescript

Không dùng console.log — nó không có level, không structured, không thể search trong production.

6. Security Headers#

import helmet from 'helmet';

app.use(helmet());
// Tự động thêm: CSP, X-Frame-Options, X-Content-Type-Options, HSTS...
typescript

7. Graceful Shutdown#

process.on('SIGTERM', async () => {
  logger.info('SIGTERM received. Shutting down gracefully...');

  server.close(async () => {
    await db.disconnect();
    await redis.disconnect();
    process.exit(0);
  });

  // Force exit sau 30s
  setTimeout(() => {
    logger.error('Forced shutdown');
    process.exit(1);
  }, 30_000);
});
typescript

8. Package.json — Script Hữu Ích#

{
  "scripts": {
    "dev": "tsx watch src/app.ts",
    "build": "tsc",
    "start": "node dist/app.js",
    "test": "vitest",
    "test:cov": "vitest --coverage",
    "lint": "eslint src/",
    "typecheck": "tsc --noEmit",
    "format": "prettier --write src/"
  }
}
json

9. Health Check Endpoint#

app.get('/health', async (req, res) => {
  const dbHealthy = await db.$queryRaw`SELECT 1`;
  const redisHealthy = await redis.ping();

  res.json({
    status: dbHealthy && redisHealthy ? 'healthy' : 'degraded',
    uptime: process.uptime(),
    timestamp: new Date().toISOString(),
  });
});
typescript

10. Dependencies — Luôn Cập Nhật#

# Kiểm tra outdated
npm outdated
pnpm outdated

# Cập nhật major
npx npm-check-updates -u
npm install
bash

Kết Luận#

Node.js dễ bắt đầu nhưng production đòi hỏi kỷ luật. Những practices trên giúp bạn:

  • Code dễ đọc, dễ mở rộng
  • Hạn chế crash không mong muốn
  • Dễ debug và monitoring
  • An toàn hơn trong production

Tài liệu tham khảo#