Menu
CoddyTech

The Same API

Part of the TypeScript and NestJS section of Coddy's Node.js journey. Lesson 24 of 24.

Section 3's clients call /api/v1/tasks and use ?q=, ?sort= and ?limit=. These are the last pieces between your Nest API and that contract.

app.setGlobalPrefix('api/v1') in main.ts moves every route under the prefix, with no controller changed. The query options work like section 3's, with two differences: error codes are Nest's status names, like BAD_REQUEST, and a limit above 50 counts as 50 instead of getting 400.

const byPrice = [...products].sort((a, b) => a.price - b.price);

Sort a copy: sort reorders the array it is called on, and the service's array would stay reordered for every later request. Throw BadRequestException for a field you do not sort by; your filter gives it the error shape.

challenge icon

Challenge

Easy

The clients of your section 3 API call /api/v1/tasks, and they can search, sort and choose a page size. Give this Nest API the same paths and the same options.

  1. In src/main.ts, inside bootstrap and before app.listen, set a global prefix of api/v1 with app.setGlobalPrefix. Every route moves under it: GET /tasks becomes GET /api/v1/tasks, and GET /stats becomes GET /api/v1/stats. No controller changes for this.
  2. In src/tasks/tasks.controller.ts, findAll reads three more query options. Apply them after the done filter and before the page is cut, so that total and pages count only the tasks that match.
  3. ?q= searches titles. ?q=pizza keeps only the tasks whose title contains pizza, ignoring case, so Order PIZZA for the team matches.
  4. ?sort= orders the list. ?sort=title sorts by title, A to Z, and ?sort=createdAt sorts oldest first. A - in front reverses the order, so ?sort=-title is Z to A. Compare the values with localeCompare, as in section 3; createdAt is ISO text, which sorts in time order. Any other field, like ?sort=colour, gets 400: throw a BadRequestException, and your filter turns it into the error shape. Sort a copy of the list, never the service's own array, or every later request comes back sorted.
  5. ?limit= sets how many tasks fit on a page. Without ?limit, the page size stays PAGE_SIZE (2 here). A limit above 50 counts as 50: ?limit=500 returns 50 tasks per page, not an error. This differs from section 3.
  6. page, total, pages and ?done= work as before, now with the new page size.

Try it: send GET /api/v1/tasks?sort=-title&limit=3. You get 200 and the tasks from Z to A, three to a page:

{
  "items": [
    { "id": 1, "title": "Write the README", "done": true, "createdAt": "2026-01-05T09:00:00.000Z" },
    { "id": 3, "title": "Plan the sprint", "done": false, "createdAt": "2026-01-07T08:15:00.000Z" },
    { "id": 2, "title": "Fix the login bug", "done": false, "createdAt": "2026-01-06T14:30:00.000Z" }
  ],
  "page": 1,
  "total": 3,
  "pages": 1
}

Send GET /api/v1/tasks?q=readme: only task 1, with "total": 1. Send GET /api/v1/tasks?sort=colour: 400 with { "error": { "code": "BAD_REQUEST", "message": "Cannot sort by colour" } }. Your message may be worded differently; the code is what counts. Send the old path, GET /tasks: 404 with { "error": { "code": "NOT_FOUND", "message": "Cannot GET /tasks" } }. POST and DELETE under /api/v1/tasks still need the header x-api-key: tasks-key-123.

Try it yourself

import { Body, Controller, Delete, Get, HttpCode, Inject, Param, ParseIntPipe, Post, Query, UseGuards } from '@nestjs/common';
import { ApiKeyGuard } from '../common/api-key.guard';
import { CreateTaskDto } from './dto/create-task.dto';
import { Task, TasksService } from './tasks.service';

@Controller('tasks')
export class TasksController {
    constructor(
        private readonly tasksService: TasksService,
        @Inject('PAGE_SIZE') private readonly pageSize: number,
    ) {}

    @Get()
    findAll(@Query('done') done?: string, @Query('page') page = '1') {
        const tasks = this.tasksService.findAll(done);
        const current = Number(page);
        const start = (current - 1) * this.pageSize;
        return {
            items: tasks.slice(start, start + this.pageSize),
            page: current,
            total: tasks.length,
            pages: Math.ceil(tasks.length / this.pageSize),
        };
    }

    @Get(':id')
    findOne(@Param('id', ParseIntPipe) id: number): Task {
        return this.tasksService.findOne(id);
    }

    @Post()
    @UseGuards(ApiKeyGuard)
    create(@Body() dto: CreateTaskDto): Task {
        return this.tasksService.create(dto);
    }

    @Delete(':id')
    @UseGuards(ApiKeyGuard)
    @HttpCode(204)
    remove(@Param('id', ParseIntPipe) id: number): void {
        this.tasksService.remove(id);
    }
}

All lessons in TypeScript and NestJS

Practice on your own: Express playground