The skeleton
Start the shelter service from almost nothing: settings from .env, a health check, and the cats API with every rule a client can break checked at the door: validated bodies, refused unknown properties, numeric ids, and 404s that say what was missing.
What you'll learn
- Read a specification and turn it into modules, controllers, services and DTOs without a step-by-step guide
- Load settings from .env with ConfigModule and read them with ConfigService
- Validate every body globally, refuse properties a DTO does not declare, and derive an update DTO from a create DTO
- Answer the API's errors as Nest does: 400 with the rules that failed, 404 naming what was not found, 204 for a deletion
Every course so far built one feature at a time on a small cats API, and each lesson kept to its topic: the database lessons had no accounts, the security lessons had no database. A real service has all of it at once, and the hard part is not any single feature but making them fit. This course builds one service, the Whiskers Shelter API, across six lessons, and each lesson keeps everything the previous ones built working. The lessons also change shape: you get a specification and the files it needs, not a walkthrough. The theory explains the decisions; the code is yours.
The service
By the end of the course the API looks like this:
| Route | Who | Lesson |
|---|---|---|
GET /health | anyone | 1 |
GET /cats, GET /cats/:id | anyone | 1 |
POST /cats, PATCH /cats/:id, DELETE /cats/:id | staff | 1, staff-only from 3 |
POST /cats/:id/adopt | a signed-in adopter | 2, by account from 3 |
GET /adoptions, GET /adoptions/mine | staff, the adopter | 2, 3 |
POST /auth/register, POST /auth/login, GET /auth/me | anyone, anyone, signed in | 3 |
GET /api, GET /api-json | anyone | 5 |
socket event cat:adopted | every connected screen | 5 |
Lesson 4 adds no route: it hardens all of them. Lesson 6 adds no code: you write the tests that prove the rest.
Settings belong in configuration
The shelter's name, and from lesson 3 a signing secret and the cost of hashing passwords, differ between a laptop and production. ConfigModule.forRoot() reads .env into ConfigService, and isGlobal: true makes it injectable everywhere without importing the module again. Read a value with config.getOrThrow('SHELTER_NAME'): a missing setting should stop the service at the first request that needs it, not quietly become undefined.
Validation at the door
One ValidationPipe in main.ts validates every body against its DTO. Three options turn it from a checker into a gate:
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }));
whitelist strips properties the DTO does not declare; forbidNonWhitelisted refuses them instead, so a client that sends owner learns it is not part of the API rather than wondering why it was ignored. transform hands the handler an instance of the DTO class and converts route parameters to their declared types.
An update accepts any subset of a creation's fields under the same rules. Writing that class twice drifts the day one rule changes. PartialType(CreateCatDto) from @nestjs/mapped-types derives it, rules included, every property optional.
Errors that say what happened
A client reads errors more often than successes. ParseIntPipe on :id answers abc with a 400 before your code runs. A cat that does not exist is a NotFoundException with a message naming it, Cat #9 not found, which Nest turns into a 404 with that message. A deletion answers 204 with no body, set with @HttpCode(204), because there is nothing left to describe.
Your task
The starter boots and lists two cats; the rest is yours. The graded requests are the specification, and each description says what the request proves.
- Settings from
.envfor the whole app, andGET /healthanswering{ status: 'ok', shelter }with the name from it. - Every body validated, unknown properties refused, parameters converted.
- A new cat has a name of 2 to 30 characters, a whole-number age from 0 to 30 and an optional breed; an update carries any of those under the same rules.
- The cats routes: the list, filtered by
?breed=; one cat by numeric id, 404Cat #9 not foundwhen missing; create (ids go on from 3, a missing breed isnull); update; remove with 204.
When it fails
Nest can't resolve dependencies of the AppController (?):ConfigServiceis not available;ConfigModule.forRoot()is missing from the imports.property age should not existon a validPATCH:UpdateCatDtodeclares no properties, so the whitelist refuses all of them. Derive it withPartialType.Cat #NaN not foundfor/cats/abc, a 404 where a 400 belongs:transformconverted the id to a number without a pipe to refuse it. AddParseIntPipe.owneris accepted, or dropped without a word:forbidNonWhitelistedis not set.
Remember
- Settings live in
.env, read throughConfigService, withgetOrThrowfor the ones the service cannot run without. whitelist,forbidNonWhitelistedandtransformmake theValidationPipea gate, not a filter.- Derive update DTOs with
PartialTypeso the rules live once. - An error names what was wrong: 400 with the rules, 404 with the id, 204 for nothing to say.
Stuck? Show a hint
main.ts: app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })). app.module.ts: ConfigModule.forRoot({ isGlobal: true }) and AppController. GET /health reads config.getOrThrow('SHELTER_NAME'). CreateCatDto: @IsString() @Length(2, 30), @IsInt() @Min(0) @Max(30), @IsOptional() @IsString(). UpdateCatDto extends PartialType(CreateCatDto) from @nestjs/mapped-types. ParseIntPipe on :id, @HttpCode(204) on delete.