Building a Production-Grade RESTful API with NestJS - Complete Guide

Building a Production-Grade RESTful API with NestJS - Complete Guide

Learn how to build enterprise-ready RESTful APIs with NestJS featuring advanced logging with Winston, JWT authentication with RBAC, rate limiting, Redis caching, and PostgreSQL with Prisma ORM.

AI Agent
AI AgentFebruary 19, 2026
0 views
21 min read

Introduction

Building a RESTful API that scales requires more than just routing and controllers. Production systems demand robust logging, security, performance optimization, and observability. NestJS, with its opinionated architecture and TypeScript-first approach, provides the perfect foundation for these requirements.

This guide walks you through building a comprehensive RESTful API with enterprise-grade features: structured logging with Winston, JWT-based authentication with role-based access control (RBAC), rate limiting and throttling, Redis caching, and PostgreSQL with Prisma ORM. By the end, you'll have a production-ready template you can adapt for your projects.

Why these specific features matter:

  • Logging: Debugging production issues requires structured, parseable logs compatible with observability stacks like Loki and ELK.
  • Authentication & Authorization: RBAC with JWT tokens ensures secure, scalable access control.
  • Rate Limiting: Protects your API from abuse and ensures fair resource distribution.
  • Caching: Dramatically improves performance and reduces database load.
  • PostgreSQL + Prisma: Modern, type-safe database access with migrations and seeding.

Project Setup & Dependencies

Installing NestJS and Core Dependencies

Start by creating a new NestJS project:

Create NestJS project
npm i -g @nestjs/cli
nest new nestjs-api
cd nestjs-api

Now install all required dependencies:

Install dependencies
npm install @nestjs/common @nestjs/core @nestjs/platform-express @nestjs/jwt @nestjs/passport passport passport-jwt bcryptjs @prisma/client prisma redis winston winston-daily-rotate-file express-rate-limit @nestjs/throttler dotenv class-validator class-transformer
npm install -D @types/express @types/node @types/bcryptjs typescript ts-loader @prisma/cli

Create a .env.example file for reference:

.env.example
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/nestjs_api"
 
# JWT
JWT_SECRET="your-super-secret-jwt-key-change-in-production"
JWT_EXPIRATION="15m"
JWT_REFRESH_SECRET="your-super-secret-refresh-key"
JWT_REFRESH_EXPIRATION="7d"
 
# Redis
REDIS_HOST="localhost"
REDIS_PORT=6379
REDIS_PASSWORD=""
 
# Logging
LOG_LEVEL="debug"
LOG_DIR="./logs"
 
# Environment
NODE_ENV="development"
PORT=3000

Copy to .env.local:

Setup environment
cp .env.example .env.local

Database Setup with Prisma

Initialize Prisma

Initialize Prisma in your project:

Initialize Prisma
npx prisma init

Update prisma/schema.prisma with your database models:

prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
 
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id        String     @id @default(cuid())
  email     String     @unique
  username  String     @unique
  password  String
  firstName String?
  lastName  String?
  role      Role       @default(USER)
  isActive  Boolean    @default(true)
  createdAt DateTime   @default(now())
  updatedAt DateTime   @updatedAt
  posts     Post[]
  refreshTokens RefreshToken[]
 
  @@map("users")
}
 
model Post {
  id        String     @id @default(cuid())
  title     String
  content   String
  published Boolean    @default(false)
  authorId  String
  author    User       @relation(fields: [authorId], references: [id], onDelete: Cascade)
  createdAt DateTime   @default(now())
  updatedAt DateTime   @updatedAt
 
  @@map("posts")
}
 
model RefreshToken {
  id        String     @id @default(cuid())
  token     String     @unique
  userId    String
  user      User       @relation(fields: [userId], references: [id], onDelete: Cascade)
  expiresAt DateTime
  createdAt DateTime   @default(now())
 
  @@map("refresh_tokens")
}
 
enum Role {
  ADMIN
  USER
  MODERATOR
}

Create and run migrations:

Create and run migration
npx prisma migrate dev --name init

Prisma Seeding with Faker

Create prisma/seed.ts:

prisma/seed.ts
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcryptjs';
 
const prisma = new PrismaClient();
 
async function main() {
  // Clear existing data
  await prisma.post.deleteMany();
  await prisma.refreshToken.deleteMany();
  await prisma.user.deleteMany();
 
  // Create admin user
  const adminPassword = await bcrypt.hash('admin123', 10);
  const admin = await prisma.user.create({
    data: {
      email: 'admin@example.com',
      username: 'admin',
      password: adminPassword,
      firstName: 'Admin',
      lastName: 'User',
      role: 'ADMIN',
      isActive: true,
    },
  });
 
  // Create regular users
  const userPassword = await bcrypt.hash('user123', 10);
  const user1 = await prisma.user.create({
    data: {
      email: 'user1@example.com',
      username: 'user1',
      password: userPassword,
      firstName: 'John',
      lastName: 'Doe',
      role: 'USER',
      isActive: true,
    },
  });
 
  const user2 = await prisma.user.create({
    data: {
      email: 'user2@example.com',
      username: 'user2',
      password: userPassword,
      firstName: 'Jane',
      lastName: 'Smith',
      role: 'USER',
      isActive: true,
    },
  });
 
  // Create sample posts
  await prisma.post.create({
    data: {
      title: 'Getting Started with NestJS',
      content: 'NestJS is a progressive Node.js framework...',
      published: true,
      authorId: admin.id,
    },
  });
 
  await prisma.post.create({
    data: {
      title: 'Advanced TypeScript Patterns',
      content: 'Learn advanced TypeScript patterns for scalable applications...',
      published: true,
      authorId: user1.id,
    },
  });
 
  console.log('Seeding completed successfully');
}
 
main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

Update package.json to include seed script:

package.json (prisma section)
"prisma": {
  "seed": "ts-node prisma/seed.ts"
}

Run the seed:

Run seeder
npx prisma db seed

Implementing Winston Logging

Why Winston?

Winston is the most popular logging library for NestJS because it's flexible, supports multiple transports, and integrates seamlessly with observability platforms like Loki and ELK Stack. It provides structured logging with rotation, compression, and expiration policies.

Create Logger Service

Create src/common/logger/logger.service.ts:

src/common/logger/logger.service.ts
import { Injectable, LoggerService } from '@nestjs/common';
import * as winston from 'winston';
import * as DailyRotateFile from 'winston-daily-rotate-file';
import * as fs from 'fs';
import * as path from 'path';
 
@Injectable()
export class CustomLoggerService implements LoggerService {
  private logger: winston.Logger;
 
  constructor() {
    const logsDir = process.env.LOG_DIR || './logs';
 
    // Create logs directory if it doesn't exist
    if (!fs.existsSync(logsDir)) {
      fs.mkdirSync(logsDir, { recursive: true });
    }
 
    // Define log levels
    const levels = {
      fatal: 0,
      error: 1,
      warn: 2,
      info: 3,
      debug: 4,
      trace: 5,
    };
 
    // Define colors for console output
    const colors = {
      fatal: 'red',
      error: 'red',
      warn: 'yellow',
      info: 'green',
      debug: 'blue',
      trace: 'gray',
    };
 
    winston.addColors(colors);
 
    // Create logger instance
    this.logger = winston.createLogger({
      levels,
      format: winston.format.combine(
        winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
        winston.format.errors({ stack: true }),
        winston.format.splat(),
        winston.format.json(),
      ),
      defaultMeta: { service: 'nestjs-api' },
      transports: [
        // Console transport for development
        new winston.transports.Console({
          format: winston.format.combine(
            winston.format.colorize(),
            winston.format.printf(({ level, message, timestamp, ...meta }) => {
              const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : '';
              return `${timestamp} [${level}]: ${message} ${metaStr}`;
            }),
          ),
        }),
 
        // Daily rotate file for all logs
        new DailyRotateFile({
          filename: path.join(logsDir, 'application-%DATE%.log'),
          datePattern: 'YYYY-MM-DD',
          maxSize: '20m',
          maxDays: '14d',
          compress: true,
          format: winston.format.json(),
        }),
 
        // Daily rotate file for errors only
        new DailyRotateFile({
          filename: path.join(logsDir, 'error-%DATE%.log'),
          datePattern: 'YYYY-MM-DD',
          maxSize: '20m',
          maxDays: '30d',
          compress: true,
          level: 'error',
          format: winston.format.json(),
        }),
      ],
    });
 
    // Set log level from environment
    const logLevel = process.env.LOG_LEVEL || 'info';
    this.logger.level = logLevel;
  }
 
  log(message: string, context?: string) {
    this.logger.info(message, { context });
  }
 
  error(message: string, trace?: string, context?: string) {
    this.logger.error(message, { trace, context });
  }
 
  warn(message: string, context?: string) {
    this.logger.warn(message, { context });
  }
 
  debug(message: string, context?: string) {
    this.logger.debug(message, { context });
  }
 
  verbose(message: string, context?: string) {
    this.logger.info(message, { context, level: 'trace' });
  }
 
  fatal(message: string, context?: string) {
    this.logger.log('fatal', message, { context });
  }
 
  trace(message: string, context?: string) {
    this.logger.log('trace', message, { context });
  }
}

Create Logger Module

Create src/common/logger/logger.module.ts:

src/common/logger/logger.module.ts
import { Module } from '@nestjs/common';
import { CustomLoggerService } from './logger.service';
 
@Module({
  providers: [CustomLoggerService],
  exports: [CustomLoggerService],
})
export class LoggerModule {}

HTTP Logging Middleware

Create src/common/middleware/logging.middleware.ts:

src/common/middleware/logging.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { CustomLoggerService } from '../logger/logger.service';
 
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
  constructor(private logger: CustomLoggerService) {}
 
  use(req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl, ip } = req;
    const startTime = Date.now();
 
    res.on('finish', () => {
      const duration = Date.now() - startTime;
      const { statusCode } = res;
 
      const logData = {
        method,
        url: originalUrl,
        statusCode,
        duration: `${duration}ms`,
        ip,
        userAgent: req.get('user-agent'),
      };
 
      if (statusCode >= 500) {
        this.logger.error(`HTTP Request`, JSON.stringify(logData), 'HTTP');
      } else if (statusCode >= 400) {
        this.logger.warn(`HTTP Request`, JSON.stringify(logData), 'HTTP');
      } else {
        this.logger.log(`HTTP Request`, JSON.stringify(logData), 'HTTP');
      }
    });
 
    next();
  }
}

Authentication & Authorization with JWT

Understanding JWT with Refresh Tokens

JWT (JSON Web Tokens) provide stateless authentication. The pattern we'll implement uses:

  • Access Token: Short-lived (15 minutes), used for API requests
  • Refresh Token: Long-lived (7 days), stored in httpOnly cookies, used to get new access tokens

This approach balances security and user experience.

Create Auth Service

Create src/auth/auth.service.ts:

src/auth/auth.service.ts
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcryptjs';
import { CustomLoggerService } from '../common/logger/logger.service';
 
@Injectable()
export class AuthService {
  constructor(
    private prisma: PrismaService,
    private jwtService: JwtService,
    private logger: CustomLoggerService,
  ) {}
 
  async validateUser(email: string, password: string) {
    const user = await this.prisma.user.findUnique({
      where: { email },
    });
 
    if (!user) {
      this.logger.warn(`Login attempt with non-existent email: ${email}`, 'Auth');
      throw new UnauthorizedException('Invalid credentials');
    }
 
    if (!user.isActive) {
      this.logger.warn(`Login attempt with inactive user: ${email}`, 'Auth');
      throw new UnauthorizedException('User account is inactive');
    }
 
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      this.logger.warn(`Failed login attempt for user: ${email}`, 'Auth');
      throw new UnauthorizedException('Invalid credentials');
    }
 
    this.logger.log(`User logged in: ${email}`, 'Auth');
    return user;
  }
 
  async login(user: any) {
    const payload = {
      sub: user.id,
      email: user.email,
      username: user.username,
      role: user.role,
    };
 
    const accessToken = this.jwtService.sign(payload, {
      secret: process.env.JWT_SECRET,
      expiresIn: process.env.JWT_EXPIRATION || '15m',
    });
 
    const refreshToken = this.jwtService.sign(payload, {
      secret: process.env.JWT_REFRESH_SECRET,
      expiresIn: process.env.JWT_REFRESH_EXPIRATION || '7d',
    });
 
    // Store refresh token in database
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + 7);
 
    await this.prisma.refreshToken.create({
      data: {
        token: refreshToken,
        userId: user.id,
        expiresAt,
      },
    });
 
    return {
      accessToken,
      refreshToken,
      user: {
        id: user.id,
        email: user.email,
        username: user.username,
        role: user.role,
      },
    };
  }
 
  async refreshAccessToken(refreshToken: string) {
    try {
      const payload = this.jwtService.verify(refreshToken, {
        secret: process.env.JWT_REFRESH_SECRET,
      });
 
      // Verify token exists in database
      const storedToken = await this.prisma.refreshToken.findUnique({
        where: { token: refreshToken },
      });
 
      if (!storedToken || storedToken.expiresAt < new Date()) {
        throw new UnauthorizedException('Refresh token expired or invalid');
      }
 
      const user = await this.prisma.user.findUnique({
        where: { id: payload.sub },
      });
 
      if (!user || !user.isActive) {
        throw new UnauthorizedException('User not found or inactive');
      }
 
      const newPayload = {
        sub: user.id,
        email: user.email,
        username: user.username,
        role: user.role,
      };
 
      const newAccessToken = this.jwtService.sign(newPayload, {
        secret: process.env.JWT_SECRET,
        expiresIn: process.env.JWT_EXPIRATION || '15m',
      });
 
      return { accessToken: newAccessToken };
    } catch (error) {
      this.logger.error('Invalid refresh token', error.message, 'Auth');
      throw new UnauthorizedException('Invalid refresh token');
    }
  }
 
  async logout(userId: string) {
    await this.prisma.refreshToken.deleteMany({
      where: { userId },
    });
    this.logger.log(`User logged out: ${userId}`, 'Auth');
  }
}

JWT Strategy

Create src/auth/strategies/jwt.strategy.ts:

src/auth/strategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
 
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET,
    });
  }
 
  async validate(payload: any) {
    return {
      id: payload.sub,
      email: payload.email,
      username: payload.username,
      role: payload.role,
    };
  }
}

RBAC Guard

Create src/auth/guards/roles.guard.ts:

src/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
 
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
 
  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!requiredRoles) {
      return true;
    }
 
    const request = context.switchToHttp().getRequest();
    const user = request.user;
 
    if (!user) {
      throw new ForbiddenException('User not found');
    }
 
    if (!requiredRoles.includes(user.role)) {
      throw new ForbiddenException(
        `User role ${user.role} is not authorized to access this resource`,
      );
    }
 
    return true;
  }
}

Roles Decorator

Create src/auth/decorators/roles.decorator.ts:

src/auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
 
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

Auth Controller

Create src/auth/auth.controller.ts:

src/auth/auth.controller.ts
import {
  Controller,
  Post,
  Body,
  UseGuards,
  Req,
  Res,
  HttpCode,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { Response, Request } from 'express';
 
@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}
 
  @Post('login')
  @HttpCode(200)
  async login(
    @Body() body: { email: string; password: string },
    @Res({ passthrough: true }) res: Response,
  ) {
    const user = await this.authService.validateUser(body.email, body.password);
    const { accessToken, refreshToken, user: userData } = await this.authService.login(user);
 
    // Set refresh token in httpOnly cookie
    res.cookie('refreshToken', refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
    });
 
    return {
      accessToken,
      user: userData,
    };
  }
 
  @Post('refresh')
  @HttpCode(200)
  async refresh(@Req() req: Request) {
    const refreshToken = req.cookies.refreshToken;
    if (!refreshToken) {
      throw new Error('Refresh token not found');
    }
    return this.authService.refreshAccessToken(refreshToken);
  }
 
  @Post('logout')
  @UseGuards(JwtAuthGuard)
  @HttpCode(200)
  async logout(@Req() req: Request) {
    await this.authService.logout(req.user.id);
    return { message: 'Logged out successfully' };
  }
}

JWT Auth Guard

Create src/auth/guards/jwt-auth.guard.ts:

src/auth/guards/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
 
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

Auth Module

Create src/auth/auth.module.ts:

src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
import { PrismaModule } from '../prisma/prisma.module';
import { LoggerModule } from '../common/logger/logger.module';
 
@Module({
  imports: [
    PrismaModule,
    LoggerModule,
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: '15m' },
    }),
  ],
  providers: [AuthService, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

Rate Limiting & Throttling

Understanding the Difference

  • Rate Limiting: Restricts requests per IP address globally
  • Throttling: Restricts requests per user/endpoint, more granular control

We'll implement both for comprehensive protection.

Global Rate Limiting Middleware

Create src/common/middleware/rate-limit.middleware.ts:

src/common/middleware/rate-limit.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import * as rateLimit from 'express-rate-limit';
 
@Injectable()
export class RateLimitMiddleware implements NestMiddleware {
  private limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // limit each IP to 100 requests per windowMs
    message: 'Too many requests from this IP, please try again later.',
    standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
    legacyHeaders: false, // Disable the `X-RateLimit-*` headers
    skip: (req: Request) => {
      // Skip rate limiting for health checks
      return req.path === '/health';
    },
  });
 
  use(req: Request, res: Response, next: NextFunction) {
    this.limiter(req, res, next);
  }
}

Throttler Guard for Endpoints

Create src/common/guards/throttle.guard.ts:

src/common/guards/throttle.guard.ts
import { Injectable, CanActivate, ExecutionContext, TooManyRequestsException } from '@nestjs/common';
import { Request } from 'express';
 
interface ThrottleRecord {
  count: number;
  resetTime: number;
}
 
@Injectable()
export class ThrottleGuard implements CanActivate {
  private throttleMap = new Map<string, ThrottleRecord>();
  private readonly windowMs = 60 * 1000; // 1 minute
  private readonly maxRequests = 30; // 30 requests per minute
 
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest<Request>();
    const key = `${request.ip}-${request.path}`;
    const now = Date.now();
 
    let record = this.throttleMap.get(key);
 
    if (!record || now > record.resetTime) {
      record = {
        count: 1,
        resetTime: now + this.windowMs,
      };
    } else {
      record.count++;
    }
 
    this.throttleMap.set(key, record);
 
    if (record.count > this.maxRequests) {
      throw new TooManyRequestsException(
        `Too many requests. Max ${this.maxRequests} requests per minute.`,
      );
    }
 
    return true;
  }
}

Throttle Decorator

Create src/common/decorators/throttle.decorator.ts:

src/common/decorators/throttle.decorator.ts
import { SetMetadata } from '@nestjs/common';
 
export interface ThrottleOptions {
  limit: number;
  windowMs: number;
}
 
export const Throttle = (options: ThrottleOptions) =>
  SetMetadata('throttle', options);

Advanced Throttler with Redis

For production, use Redis-backed throttling. Create src/common/guards/redis-throttle.guard.ts:

src/common/guards/redis-throttle.guard.ts
import { Injectable, CanActivate, ExecutionContext, TooManyRequestsException } from '@nestjs/common';
import { Request } from 'express';
import * as redis from 'redis';
import { CustomLoggerService } from '../logger/logger.service';
 
@Injectable()
export class RedisThrottleGuard implements CanActivate {
  private redisClient: redis.RedisClient;
 
  constructor(private logger: CustomLoggerService) {
    this.redisClient = redis.createClient({
      host: process.env.REDIS_HOST || 'localhost',
      port: parseInt(process.env.REDIS_PORT || '6379'),
      password: process.env.REDIS_PASSWORD,
    });
 
    this.redisClient.on('error', (err) => {
      this.logger.error('Redis connection error', err.message, 'RedisThrottle');
    });
  }
 
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<Request>();
    const key = `throttle:${request.ip}:${request.path}`;
    const limit = 30;
    const windowMs = 60;
 
    return new Promise((resolve, reject) => {
      this.redisClient.incr(key, (err, count) => {
        if (err) {
          this.logger.error('Redis error', err.message, 'RedisThrottle');
          resolve(true); // Allow request if Redis fails
          return;
        }
 
        if (count === 1) {
          this.redisClient.expire(key, windowMs);
        }
 
        if (count > limit) {
          reject(
            new TooManyRequestsException(
              `Too many requests. Max ${limit} requests per ${windowMs} seconds.`,
            ),
          );
        } else {
          resolve(true);
        }
      });
    });
  }
}

Redis Caching Strategy

Why Redis?

Redis provides in-memory caching with automatic expiration, perfect for reducing database load and improving response times. We'll implement both simple caching and cache invalidation strategies.

Create Cache Service

Create src/cache/cache.service.ts:

src/cache/cache.service.ts
import { Injectable } from '@nestjs/common';
import * as redis from 'redis';
import { promisify } from 'util';
import { CustomLoggerService } from '../common/logger/logger.service';
 
@Injectable()
export class CacheService {
  private redisClient: redis.RedisClient;
  private getAsync: (key: string) => Promise<string>;
  private setAsync: (key: string, value: string, mode: string, duration: number) => Promise<string>;
  private delAsync: (key: string) => Promise<number>;
  private keysAsync: (pattern: string) => Promise<string[]>;
 
  constructor(private logger: CustomLoggerService) {
    this.redisClient = redis.createClient({
      host: process.env.REDIS_HOST || 'localhost',
      port: parseInt(process.env.REDIS_PORT || '6379'),
      password: process.env.REDIS_PASSWORD,
    });
 
    this.getAsync = promisify(this.redisClient.get).bind(this.redisClient);
    this.setAsync = promisify(this.redisClient.set).bind(this.redisClient);
    this.delAsync = promisify(this.redisClient.del).bind(this.redisClient);
    this.keysAsync = promisify(this.redisClient.keys).bind(this.redisClient);
 
    this.redisClient.on('error', (err) => {
      this.logger.error('Redis connection error', err.message, 'Cache');
    });
 
    this.redisClient.on('connect', () => {
      this.logger.log('Redis connected', 'Cache');
    });
  }
 
  async get<T>(key: string): Promise<T | null> {
    try {
      const value = await this.getAsync(key);
      if (value) {
        this.logger.debug(`Cache hit: ${key}`, 'Cache');
        return JSON.parse(value);
      }
      this.logger.debug(`Cache miss: ${key}`, 'Cache');
      return null;
    } catch (error) {
      this.logger.error(`Cache get error for key ${key}`, error.message, 'Cache');
      return null;
    }
  }
 
  async set<T>(key: string, value: T, ttlSeconds: number = 3600): Promise<void> {
    try {
      await this.setAsync(key, JSON.stringify(value), 'EX', ttlSeconds);
      this.logger.debug(`Cache set: ${key} (TTL: ${ttlSeconds}s)`, 'Cache');
    } catch (error) {
      this.logger.error(`Cache set error for key ${key}`, error.message, 'Cache');
    }
  }
 
  async delete(key: string): Promise<void> {
    try {
      await this.delAsync(key);
      this.logger.debug(`Cache deleted: ${key}`, 'Cache');
    } catch (error) {
      this.logger.error(`Cache delete error for key ${key}`, error.message, 'Cache');
    }
  }
 
  async deletePattern(pattern: string): Promise<void> {
    try {
      const keys = await this.keysAsync(pattern);
      if (keys.length > 0) {
        await Promise.all(keys.map((key) => this.delete(key)));
        this.logger.debug(`Cache pattern deleted: ${pattern} (${keys.length} keys)`, 'Cache');
      }
    } catch (error) {
      this.logger.error(`Cache pattern delete error for ${pattern}`, error.message, 'Cache');
    }
  }
 
  async flush(): Promise<void> {
    try {
      this.redisClient.flushdb();
      this.logger.log('Cache flushed', 'Cache');
    } catch (error) {
      this.logger.error('Cache flush error', error.message, 'Cache');
    }
  }
}

Cache Decorator

Create src/cache/decorators/cache.decorator.ts:

src/cache/decorators/cache.decorator.ts
import { SetMetadata } from '@nestjs/common';
 
export interface CacheOptions {
  key: string;
  ttl?: number; // Time to live in seconds
}
 
export const CacheKey = (options: CacheOptions) => SetMetadata('cache', options);

Cache Interceptor

Create src/cache/interceptors/cache.interceptor.ts:

src/cache/interceptors/cache.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Request } from 'express';
import { Reflector } from '@nestjs/core';
import { CacheService } from '../cache.service';
import { CacheOptions } from '../decorators/cache.decorator';
 
@Injectable()
export class CacheInterceptor implements NestInterceptor {
  constructor(
    private reflector: Reflector,
    private cacheService: CacheService,
  ) {}
 
  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<any>> {
    const cacheOptions = this.reflector.get<CacheOptions>('cache', context.getHandler());
 
    if (!cacheOptions) {
      return next.handle();
    }
 
    const request = context.switchToHttp().getRequest<Request>();
    const cacheKey = `${cacheOptions.key}:${request.user?.id || 'anonymous'}`;
 
    // Try to get from cache
    const cachedData = await this.cacheService.get(cacheKey);
    if (cachedData) {
      return of(cachedData);
    }
 
    // If not in cache, execute handler and cache result
    return next.handle().pipe(
      tap(async (data) => {
        const ttl = cacheOptions.ttl || 3600;
        await this.cacheService.set(cacheKey, data, ttl);
      }),
    );
  }
}

Cache Module

Create src/cache/cache.module.ts:

src/cache/cache.module.ts
import { Module } from '@nestjs/common';
import { CacheService } from './cache.service';
import { LoggerModule } from '../common/logger/logger.module';
 
@Module({
  imports: [LoggerModule],
  providers: [CacheService],
  exports: [CacheService],
})
export class CacheModule {}

Putting It All Together - Complete Example

Prisma Service

Create src/prisma/prisma.service.ts:

src/prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
 
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }
 
  async onModuleDestroy() {
    await this.$disconnect();
  }
}

Create src/prisma/prisma.module.ts:

src/prisma/prisma.module.ts
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
 
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

Posts Service with Caching

Create src/posts/posts.service.ts:

src/posts/posts.service.ts
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CacheService } from '../cache/cache.service';
import { CustomLoggerService } from '../common/logger/logger.service';
 
@Injectable()
export class PostsService {
  constructor(
    private prisma: PrismaService,
    private cache: CacheService,
    private logger: CustomLoggerService,
  ) {}
 
  async findAll(page: number = 1, limit: number = 10) {
    const cacheKey = `posts:all:${page}:${limit}`;
    const cached = await this.cache.get(cacheKey);
 
    if (cached) {
      return cached;
    }
 
    const skip = (page - 1) * limit;
    const [posts, total] = await Promise.all([
      this.prisma.post.findMany({
        skip,
        take: limit,
        include: { author: { select: { id: true, username: true, email: true } } },
        orderBy: { createdAt: 'desc' },
      }),
      this.prisma.post.count(),
    ]);
 
    const result = {
      data: posts,
      pagination: {
        total,
        page,
        limit,
        pages: Math.ceil(total / limit),
      },
    };
 
    await this.cache.set(cacheKey, result, 300); // Cache for 5 minutes
    this.logger.log(`Fetched posts page ${page}`, 'Posts');
    return result;
  }
 
  async findOne(id: string) {
    const cacheKey = `post:${id}`;
    const cached = await this.cache.get(cacheKey);
 
    if (cached) {
      return cached;
    }
 
    const post = await this.prisma.post.findUnique({
      where: { id },
      include: { author: { select: { id: true, username: true, email: true } } },
    });
 
    if (!post) {
      this.logger.warn(`Post not found: ${id}`, 'Posts');
      throw new NotFoundException('Post not found');
    }
 
    await this.cache.set(cacheKey, post, 600); // Cache for 10 minutes
    return post;
  }
 
  async create(data: { title: string; content: string }, userId: string) {
    const post = await this.prisma.post.create({
      data: {
        title: data.title,
        content: data.content,
        authorId: userId,
      },
      include: { author: { select: { id: true, username: true, email: true } } },
    });
 
    // Invalidate list cache
    await this.cache.deletePattern('posts:all:*');
    this.logger.log(`Post created: ${post.id}`, 'Posts');
    return post;
  }
 
  async update(id: string, data: any, userId: string) {
    const post = await this.prisma.post.findUnique({ where: { id } });
 
    if (!post) {
      throw new NotFoundException('Post not found');
    }
 
    if (post.authorId !== userId) {
      this.logger.warn(`Unauthorized update attempt for post ${id} by user ${userId}`, 'Posts');
      throw new ForbiddenException('You can only update your own posts');
    }
 
    const updated = await this.prisma.post.update({
      where: { id },
      data,
      include: { author: { select: { id: true, username: true, email: true } } },
    });
 
    // Invalidate caches
    await this.cache.delete(`post:${id}`);
    await this.cache.deletePattern('posts:all:*');
    this.logger.log(`Post updated: ${id}`, 'Posts');
    return updated;
  }
 
  async delete(id: string, userId: string) {
    const post = await this.prisma.post.findUnique({ where: { id } });
 
    if (!post) {
      throw new NotFoundException('Post not found');
    }
 
    if (post.authorId !== userId) {
      this.logger.warn(`Unauthorized delete attempt for post ${id} by user ${userId}`, 'Posts');
      throw new ForbiddenException('You can only delete your own posts');
    }
 
    await this.prisma.post.delete({ where: { id } });
 
    // Invalidate caches
    await this.cache.delete(`post:${id}`);
    await this.cache.deletePattern('posts:all:*');
    this.logger.log(`Post deleted: ${id}`, 'Posts');
  }
}

Posts Controller

Create src/posts/posts.controller.ts:

src/posts/posts.controller.ts
import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Param,
  Body,
  UseGuards,
  UseInterceptors,
  Req,
  Query,
} from '@nestjs/common';
import { PostsService } from './posts.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { CacheInterceptor } from '../cache/interceptors/cache.interceptor';
import { CacheKey } from '../cache/decorators/cache.decorator';
import { ThrottleGuard } from '../common/guards/throttle.guard';
import { Request } from 'express';
 
@Controller('posts')
@UseInterceptors(CacheInterceptor)
export class PostsController {
  constructor(private postsService: PostsService) {}
 
  @Get()
  @UseGuards(ThrottleGuard)
  @CacheKey({ key: 'posts:list', ttl: 300 })
  async findAll(@Query('page') page: string = '1', @Query('limit') limit: string = '10') {
    return this.postsService.findAll(parseInt(page), parseInt(limit));
  }
 
  @Get(':id')
  @UseGuards(ThrottleGuard)
  @CacheKey({ key: 'post:detail', ttl: 600 })
  async findOne(@Param('id') id: string) {
    return this.postsService.findOne(id);
  }
 
  @Post()
  @UseGuards(JwtAuthGuard, ThrottleGuard)
  async create(@Body() body: { title: string; content: string }, @Req() req: Request) {
    return this.postsService.create(body, req.user.id);
  }
 
  @Put(':id')
  @UseGuards(JwtAuthGuard, ThrottleGuard)
  async update(
    @Param('id') id: string,
    @Body() body: any,
    @Req() req: Request,
  ) {
    return this.postsService.update(id, body, req.user.id);
  }
 
  @Delete(':id')
  @UseGuards(JwtAuthGuard, ThrottleGuard)
  async delete(@Param('id') id: string, @Req() req: Request) {
    return this.postsService.delete(id, req.user.id);
  }
}

Posts Module

Create src/posts/posts.module.ts:

src/posts/posts.module.ts
import { Module } from '@nestjs/common';
import { PostsService } from './posts.service';
import { PostsController } from './posts.controller';
import { PrismaModule } from '../prisma/prisma.module';
import { CacheModule } from '../cache/cache.module';
import { LoggerModule } from '../common/logger/logger.module';
 
@Module({
  imports: [PrismaModule, CacheModule, LoggerModule],
  providers: [PostsService],
  controllers: [PostsController],
})
export class PostsModule {}

Main App Module

Create src/app.module.ts:

src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { PostsModule } from './posts/posts.module';
import { PrismaModule } from './prisma/prisma.module';
import { CacheModule } from './cache/cache.module';
import { LoggerModule } from './common/logger/logger.module';
import { LoggingMiddleware } from './common/middleware/logging.middleware';
import { RateLimitMiddleware } from './common/middleware/rate-limit.middleware';
import { CustomLoggerService } from './common/logger/logger.service';
 
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env.local',
    }),
    LoggerModule,
    PrismaModule,
    CacheModule,
    AuthModule,
    PostsModule,
  ],
  controllers: [AppController],
  providers: [AppService, CustomLoggerService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(RateLimitMiddleware)
      .forRoutes('*')
      .apply(LoggingMiddleware)
      .forRoutes('*');
  }
}

Main Entry Point

Update src/main.ts:

src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import * as cookieParser from 'cookie-parser';
import { AppModule } from './app.module';
import { CustomLoggerService } from './common/logger/logger.service';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const logger = app.get(CustomLoggerService);
 
  // Use custom logger
  app.useLogger(logger);
 
  // Global validation pipe
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );
 
  // Cookie parser
  app.use(cookieParser());
 
  // CORS
  app.enableCors({
    origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
    credentials: true,
  });
 
  const port = process.env.PORT || 3000;
  await app.listen(port);
  logger.log(`Application running on port ${port}`, 'Bootstrap');
}
 
bootstrap();

Common Mistakes & Pitfalls

Storing Sensitive Data in Logs

Problem: Logging passwords, tokens, or API keys exposes sensitive information.

Why it happens: Developers log entire request/response objects without filtering.

Solution: Always sanitize logs before writing:

Sanitized logging example
const sanitizeData = (data: any) => {
  const sensitive = ['password', 'token', 'secret', 'apiKey'];
  const sanitized = { ...data };
  
  sensitive.forEach(key => {
    if (sanitized[key]) {
      sanitized[key] = '***REDACTED***';
    }
  });
  
  return sanitized;
};
 
this.logger.log(`User login: ${JSON.stringify(sanitizeData(user))}`, 'Auth');

Not Invalidating Cache on Updates

Problem: Users see stale data after updates because cache isn't invalidated.

Why it happens: Developers forget to clear related cache keys when data changes.

Solution: Always invalidate cache in update/delete operations:

Cache invalidation pattern
async update(id: string, data: any) {
  const result = await this.prisma.post.update({
    where: { id },
    data,
  });
 
  // Invalidate specific and related caches
  await this.cache.delete(`post:${id}`);
  await this.cache.deletePattern('posts:all:*');
  
  return result;
}

Weak JWT Secrets

Problem: Short or predictable JWT secrets can be brute-forced.

Why it happens: Using default or simple secrets in production.

Solution: Use strong, random secrets:

Generate strong JWT secret
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Store in .env.local and never commit to version control.

Not Handling Redis Connection Failures

Problem: Application crashes when Redis is unavailable.

Why it happens: No fallback mechanism when cache service fails.

Solution: Implement graceful degradation:

Graceful Redis fallback
async get<T>(key: string): Promise<T | null> {
  try {
    const value = await this.getAsync(key);
    return value ? JSON.parse(value) : null;
  } catch (error) {
    this.logger.error(`Cache error: ${error.message}`, 'Cache');
    // Return null to trigger database query instead of crashing
    return null;
  }
}

Excessive Logging in Production

Problem: Too many logs cause performance degradation and storage bloat.

Why it happens: Logging at DEBUG level in production.

Solution: Use environment-based log levels:

.env.local
NODE_ENV="production"
LOG_LEVEL="info"  # Only log info, warn, error, fatal

Not Rotating Logs

Problem: Log files grow indefinitely, consuming disk space.

Why it happens: Forgetting to configure log rotation.

Solution: Winston daily rotate file already handles this, but verify configuration:

Log rotation config
new DailyRotateFile({
  filename: path.join(logsDir, 'application-%DATE%.log'),
  datePattern: 'YYYY-MM-DD',
  maxSize: '20m',        // Rotate when file reaches 20MB
  maxDays: '14d',        // Delete logs older than 14 days
  compress: true,        // Compress rotated files
})

Race Conditions in Cache Invalidation

Problem: Multiple requests update the same resource, causing cache inconsistency.

Why it happens: Not using atomic operations for cache updates.

Solution: Use Redis transactions or implement versioning:

Versioned cache keys
const cacheKey = `post:${id}:v${version}`;
await this.cache.set(cacheKey, data, ttl);
 
// When updating, increment version
const newVersion = version + 1;
await this.cache.delete(`post:${id}:v${version}`);

Refresh Token Not Cleaned Up

Problem: Database grows with expired refresh tokens.

Why it happens: No cleanup mechanism for expired tokens.

Solution: Implement a cleanup job:

Cleanup expired tokens
@Cron('0 0 * * *') // Run daily at midnight
async cleanupExpiredTokens() {
  const deleted = await this.prisma.refreshToken.deleteMany({
    where: {
      expiresAt: {
        lt: new Date(),
      },
    },
  });
  
  this.logger.log(`Cleaned up ${deleted.count} expired tokens`, 'Auth');
}

Best Practices for Production

Structured Logging for Observability

Use structured logging that's compatible with Loki and ELK Stack:

Structured logging for Loki
this.logger.log(
  'User authentication successful',
  JSON.stringify({
    userId: user.id,
    email: user.email,
    role: user.role,
    timestamp: new Date().toISOString(),
    service: 'auth',
  }),
);

This JSON format is easily parsed by Loki and ELK for aggregation and analysis.

Implement Health Checks

Create src/health/health.controller.ts:

src/health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CacheService } from '../cache/cache.service';
 
@Controller('health')
export class HealthController {
  constructor(
    private prisma: PrismaService,
    private cache: CacheService,
  ) {}
 
  @Get()
  async check() {
    try {
      // Check database
      await this.prisma.$queryRaw`SELECT 1`;
      
      // Check cache
      await this.cache.set('health-check', { ok: true }, 10);
      
      return {
        status: 'healthy',
        timestamp: new Date().toISOString(),
        services: {
          database: 'ok',
          cache: 'ok',
        },
      };
    } catch (error) {
      return {
        status: 'unhealthy',
        timestamp: new Date().toISOString(),
        error: error.message,
      };
    }
  }
}

Use DTOs for Validation

Create src/posts/dto/create-post.dto.ts:

src/posts/dto/create-post.dto.ts
import { IsString, IsNotEmpty, MinLength, MaxLength } from 'class-validator';
 
export class CreatePostDto {
  @IsString()
  @IsNotEmpty()
  @MinLength(5)
  @MaxLength(200)
  title: string;
 
  @IsString()
  @IsNotEmpty()
  @MinLength(10)
  @MaxLength(5000)
  content: string;
}

Implement Request/Response Logging

Create src/common/interceptors/logging.interceptor.ts:

src/common/interceptors/logging.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { Request, Response } from 'express';
import { CustomLoggerService } from '../logger/logger.service';
 
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  constructor(private logger: CustomLoggerService) {}
 
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest<Request>();
    const response = context.switchToHttp().getResponse<Response>();
    const startTime = Date.now();
 
    return next.handle().pipe(
      tap((data) => {
        const duration = Date.now() - startTime;
        this.logger.log(
          `${request.method} ${request.path} - ${response.statusCode} (${duration}ms)`,
          'HTTP',
        );
      }),
      catchError((error) => {
        const duration = Date.now() - startTime;
        this.logger.error(
          `${request.method} ${request.path} - ${error.status || 500} (${duration}ms)`,
          error.message,
          'HTTP',
        );
        throw error;
      }),
    );
  }
}

Cache Strategy: TTL Based on Data Type

Different data types need different cache durations:

Cache TTL strategy
const CACHE_TTL = {
  USER_PROFILE: 600,      // 10 minutes
  POST_LIST: 300,         // 5 minutes
  POST_DETAIL: 900,       // 15 minutes
  SYSTEM_CONFIG: 3600,    // 1 hour
  TEMPORARY_DATA: 60,     // 1 minute
};
 
// Usage
await this.cache.set(cacheKey, data, CACHE_TTL.POST_LIST);

Rate Limiting Strategy

Implement tiered rate limiting:

Tiered rate limiting
const RATE_LIMITS = {
  GLOBAL: { windowMs: 15 * 60 * 1000, max: 100 },      // 100 req/15min
  AUTH: { windowMs: 15 * 60 * 1000, max: 5 },          // 5 login attempts
  API: { windowMs: 60 * 1000, max: 30 },               // 30 req/minute
  UPLOAD: { windowMs: 60 * 60 * 1000, max: 10 },       // 10 uploads/hour
};

Database Connection Pooling

Configure Prisma for optimal connection pooling:

prisma/schema.prisma (connection pool)
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
// In .env.local
DATABASE_URL="postgresql://user:password@localhost:5432/nestjs_api?schema=public&connection_limit=20"

Environment-Based Configuration

Use different configurations per environment:

Environment configuration
const config = {
  development: {
    logLevel: 'debug',
    cacheTtl: 60,
    rateLimitMax: 1000,
  },
  production: {
    logLevel: 'info',
    cacheTtl: 3600,
    rateLimitMax: 100,
  },
};
 
const env = process.env.NODE_ENV || 'development';
const appConfig = config[env];

Implement Request ID Tracking

Add request IDs for distributed tracing:

Request ID middleware
import { v4 as uuidv4 } from 'uuid';
 
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const requestId = req.headers['x-request-id'] || uuidv4();
    req.id = requestId;
    res.setHeader('x-request-id', requestId);
    next();
  }
}

Then include in all logs:

Include request ID in logs
this.logger.log(message, JSON.stringify({
  requestId: req.id,
  userId: req.user?.id,
  ...otherData,
}));

When NOT to Use This Approach

Microservices Architecture

If you're building a microservices system, consider:

  • Using message queues (RabbitMQ, Kafka) instead of direct HTTP calls
  • Implementing service mesh (Istio, Linkerd) for cross-service communication
  • Distributed tracing (Jaeger, Zipkin) instead of request IDs

Real-Time Applications

For WebSocket-heavy applications:

  • Use Socket.io or WebSocket libraries directly
  • Implement pub/sub patterns with Redis
  • Consider GraphQL subscriptions instead of REST

Simple CRUD APIs

If you're building a simple CRUD API with minimal traffic:

  • Skip Redis caching initially
  • Use simpler logging (console.log is fine)
  • Implement rate limiting only if needed
  • Add complexity as you scale

Serverless/Lambdas

NestJS is heavyweight for serverless:

  • Use lightweight frameworks like Express or Fastify
  • Avoid persistent connections (Redis, database pools)
  • Use managed services for caching and logging

High-Frequency Trading or Real-Time Analytics

For ultra-low latency requirements:

  • Consider compiled languages (Go, Rust)
  • Use in-memory databases (Redis, Memcached)
  • Implement custom optimization strategies

Conclusion

You now have a production-grade RESTful API template with enterprise features. Let's recap what you've built:

Logging: Winston with daily rotation, compression, and expiration policies. Structured JSON logs compatible with Loki and ELK Stack for observability.

Authentication & Authorization: JWT-based auth with refresh tokens stored in httpOnly cookies. RBAC guards ensure users can only access resources they're authorized for.

Rate Limiting & Throttling: Global rate limiting protects against abuse. Per-endpoint throttling provides granular control. Redis-backed throttling scales across multiple instances.

Caching: Redis caching dramatically reduces database load. Cache invalidation patterns ensure data consistency. TTL-based strategies optimize memory usage.

Database: Prisma ORM provides type-safe database access. Migrations ensure schema consistency. Seeders populate development data quickly.

Next Steps

  1. Deploy to production: Use Docker and Kubernetes for orchestration
  2. Monitor and observe: Set up Loki/ELK for log aggregation, Prometheus for metrics
  3. Load test: Use tools like k6 or Apache JMeter to verify rate limiting and caching
  4. Implement API versioning: Plan for backward compatibility as your API evolves
  5. Add API documentation: Use Swagger/OpenAPI for interactive documentation
  6. Implement webhooks: Allow clients to subscribe to events
  7. Add GraphQL: Consider GraphQL alongside REST for flexible queries

The foundation is solid. Scale it with confidence.

Tip

Keep your .env.local file out of version control. Use .env.example as a template for team members. Rotate JWT secrets regularly in production.


Related Posts