The sun was barely up over Playa Venao, Panama, when my phone buzzed.
Luis—one of the junior devs I’d mentored back in Medellín—was frantic: “James, our Rest API is returning 500s. The NestJS controller looks fine, but something’s off. Can you jump on a call?” I was still dripping seawater after an early surf session, board tucked under my arm, but I remembered being in his shoes: staring at mysterious JSON errors while the Caribbean heat melted any remaining patience. So I hustled to a beach-side café, ordered the strongest café con leche they had, and opened VS Code. By the second sip, we’d traced the issue to a sloppy controller that mashed validation, business logic, and database calls into a single, tangled function. Today’s post distills that salty debugging session into a blueprint you can reuse—before your next wave sets roll in.


Foundations: Why NestJS Loves REST

NestJS wraps Express (or Fastify) in TypeScript and decorators, letting us write controllers that feel more like TypeScript classes than spaghetti routes. A disciplined Rest API in NestJS gives you:

Key idea: keep controllers thin—or risk the same 3 a.m. alerts Luis faced in Medellín.


The Case Study: How a 200-Line Controller Became 20

While consulting for a rideshare startup in Mexico City, I inherited a controller that managed users, trips, and payments—all in one file. Latency yawed past 600 ms, and every deploy felt like spinning a roulette wheel. We broke the monolith controller into three resources, introduced DTO validation, and delegated hairy business rules to services. Average response time dropped to 110 ms. Investors stopped asking awkward questions, and I finally had time to explore Coyoacán’s mercados without Slack anxiety.


Step-by-Step Implementation in NestJS

1 · Bootstrapping the Project

bashCopyEditnpm i -g @nestjs/cli
nest new library-api
cd library-api
nest g resource books --no-spec --crud --path src

The resource schematic scaffolds a module, controller, service, and DTOs—perfect scaffolding for a clean Rest API.

2 · Defining a DTO

typescriptCopyEdit// src/books/dto/create-book.dto.ts
import { IsString, IsInt, Min, Max } from 'class-validator';

export class CreateBookDto {
  @IsString()  readonly title: string;
  @IsString()  readonly author: string;
  @IsInt() @Min(1500) @Max(new Date().getFullYear())
  readonly year: number;
}

Why? Validation belongs near the boundary, so invalid data never reaches your service layer.

3 · Writing the Controller

typescriptCopyEdit// src/books/books.controller.ts
import {
  Controller, Get, Post, Param, Body, Put, Delete, HttpCode, HttpStatus,
} from '@nestjs/common';
import { BooksService } from './books.service';
import { CreateBookDto } from './dto/create-book.dto';
import { UpdateBookDto } from './dto/update-book.dto';

@Controller('books')
export class BooksController {
  constructor(private readonly booksService: BooksService) {}

  @Get()
  findAll() {
    return this.booksService.findAll(); // returns Promise<Book[]>
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.booksService.findOne(id); // 404 handled in service
  }

  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() dto: CreateBookDto) {
    return this.booksService.create(dto);
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() dto: UpdateBookDto) {
    return this.booksService.update(id, dto);
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id') id: string) {
    return this.booksService.remove(id);
  }
}

Notice the controller only orchestrates, delegating heavy lifting to BooksService. That keeps our Rest API lean—and unit-test friendly.

⚡ Mini-exercise: Add pagination to findAll() by injecting Query parameters for take and skip.

4 · Service Layer

typescriptCopyEdit@Injectable()
export class BooksService {
  constructor(@InjectModel(Book.name) private readonly model: Model<BookDocument>) {}

  async findAll() { return this.model.find().exec(); }

  async findOne(id: string) {
    const book = await this.model.findById(id).exec();
    if (!book) throw new NotFoundException(`Book ${id} not found`);
    return book;
  }

  async create(dto: CreateBookDto) { return this.model.create(dto); }

  async update(id: string, dto: UpdateBookDto) {
    const updated = await this.model.findByIdAndUpdate(id, dto, { new: true });
    if (!updated) throw new NotFoundException(`Book ${id} not found`);
    return updated;
  }

  async remove(id: string) {
    const result = await this.model.findByIdAndDelete(id);
    if (!result) throw new NotFoundException(`Book ${id} not found`);
  }
}

Troubles Spained in Bogotá: Common Pitfalls

SymptomRoot CauseRemedy
502 Bad Gateway spikesBlocking sync work in controllerMark CPU-heavy logic async; offload to queues
DTO validation ignoredPipes not enabled globallyIn main.ts, app.useGlobalPipes(new ValidationPipe())
Database leaks on errorMissing try/catch in serviceWrap calls, throw HttpException
N+1 query painMultiple populate() inside loopsUse aggregation or batch loaders

“Ask me how I learned this the hard way in Bangkok…”—I once forgot to enable global pipes, and invalid JSON crashed a production Rest API while I was trapped on a tuk-tuk in rush-hour rain.


Performance and Security: Keep It Tight

pgsqlCopyEditBrowser ──> Nginx ──> NestJS Cluster ──> MongoDB Replica
   ▲                                  │
   └─────────── Prometheus <──────────┘

⚡ Mini-exercise: Instrument the BooksService.findAll() method with a custom OpenTelemetry span and visualize it in Jaeger.


CI/CD: From GitHub to AWS Without Tears

yamlCopyEdit# .github/workflows/ci.yml
name: node-ci
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run lint
      - run: npm test -- --runInBand
      - run: npm run build
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/your-org/library-api:latest

In production I deploy this container to AWS Fargate behind an Application Load Balancer with HTTPS certificates via AWS ACM. Terraform modules wire up the task definition, CloudWatch log group, and secret-manager variables.


For the Curious — Beyond REST

Sidebar: GraphQL Federation in NestJS

When your Rest API grows into microservices, consider Apollo Federation with @nestjs/graphql. Each service exposes a partial schema; a gateway stitches them into a single API. Tracing and caching still apply—but schema-first thinking changes how you model resources.


Vocabulary Table

TermMeaning
Rest APIResource-oriented interface over HTTP verbs
DTOData Transfer Object: validated payload schema
PipeNestJS class that transforms/validates input
ProviderInjectable class (service, repository, etc.)
InterceptorWrapper for cross-cutting concerns (logging, caching)
ModuleLogical container for providers/controllers
GuardAuthorization layer for routes
MiddlewareFunction that runs before controllers

Wrapping Up on a Rio Rooftop

Right now I’m finishing this draft from a rooftop in Ipanema. The Atlantic hums below, and somewhere on my laptop a Jest suite is green. That’s the real payoff of a disciplined Rest API: freedom to chase sunsets while your endpoints hum along. You learned how thin NestJS controllers point to services, how pipes keep bad data out, and why observability saves your sleep. Next post we’ll version these controllers and explore feature flags—because change is the one constant when you’re coding from country to country.

Questions? War stories? Drop a comment below, and I’ll answer while the coffee’s still hot.

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x