Files
What you'll learn
- Write a provider with @Injectable() and receive it through a constructor
- Keep state in a service and see one instance shared across requests
- Throw NotFoundException and get Nest's 404 body without writing response code
Providers
A controller should be thin: it turns a request into a call and a result into a response, and nothing else. The work belongs in a provider, a class Nest constructs once and hands to whoever asks for it. Splitting the two is not ceremony. It is what lets the same logic serve an HTTP route today and a scheduled job or a message queue tomorrow, and what lets you test the logic without an HTTP request.
What makes a class a provider
Two things: the @Injectable() decorator, and an entry in a module's providers array.
@Injectable()
export class CounterService {
private count = 0;
increment(): number {
return ++this.count;
}
}
@Injectable() marks the class as something Nest may create. Nest creates it once and reuses that single instance everywhere it is injected. That is why a service can keep state in a field: count above lives as long as the application does, and every request that reaches the service sees the same number. Two controllers injecting CounterService share one counter.
In this browser runtime "as long as the application does" means until you press Run tests again, which starts a fresh application. The grader relies on that. The endpoints of a lesson run in order against one application, so a cat created by the first request is still there for the requests after it.
Asking for a provider
A class receives a provider by naming it in its constructor:
@Controller('counter')
export class CounterController {
constructor(private readonly counter: CounterService) {}
@Post()
bump() {
return { count: this.counter.increment() };
}
}
The private readonly shorthand declares the field and assigns it in one line. Nest reads the parameter's type, CounterService, finds the provider registered under that class, and passes the instance. Nothing here calls new CounterService(), and nothing should: a controller that did would get a second, private copy with its own count, and the two would disagree.
Interfaces are for the compiler only
cat.ts exports an interface:
export interface Cat {
id: number;
name: string;
age: number;
}
An interface describes a shape and disappears when TypeScript compiles. That is fine for return types and array elements, as here. It is not fine as a constructor parameter type for injection: Nest looks the type up at runtime, and at runtime there is nothing left of an interface. Inject classes.
Answering with an error
When something is not there, the service says so by throwing:
throw new NotFoundException(`Order #${id} not found`);
NotFoundException is one of Nest's built-in HttpException classes. Thrown from anywhere in a request (service, controller, pipe), it becomes a response with the matching status and a JSON body:
{ "message": "Order #7 not found", "error": "Not Found", "statusCode": 404 }
There is one for each common status: BadRequestException, UnauthorizedException, ForbiddenException, ConflictException and more. A plain Error thrown from a handler is different: the client gets a 500 Internal Server Error with a generic body, and the real message goes to the console, where you can read it. Lesson 7 looks at the machinery behind this.
Your task
CatsController is finished and already injects CatsService. Three methods of the service are left to write:
create(name, age)builds aCatwith the next id, stores it, and returns it. Ids start at 1.findOne(id)returns the cat with that id, or throwsNotFoundExceptionwith the message`Cat #${id} not found`.remove(id)takes the cat with that id out of the array and returns it. A cat that does not exist is the same 404 as infindOne, and there is a way to get that without writing the check twice.
When the tests pass, open the Endpoints tab, create a few cats by hand, then list them. The state you see is the service's array, and it lasts until the next run.
When it fails
- Status 500 with
Internal server error: a plainErrorwas thrown, probably the starter'sNot implemented. The console shows the message and the file and line it came from. - The second cat gets id 1: the id is computed from something that does not change. Keep a counter, or derive the id from the array's length.
GET /cats/99answers 200 with nothing:findOnereturnedundefinedinstead of throwing.DELETE /cats/1twice answers 200 twice:removedid not check the cat exists, or did not take it out of the array.
Remember
- A provider is an
@Injectable()class listed in a module'sproviders. Nest creates one instance and shares it. - Ask for a provider in a constructor. Never
newit. - Interfaces vanish at compile time, so they cannot be injected. Classes can.
- Throw an
HttpExceptionsubclass for anything the client should be told about.
Stuck? Show a hint
The service owns the array. create() needs an id nobody has used yet: keep a counter in a field. findOne() can use Array.prototype.find and throw NotFoundException when nothing comes back. remove() can call findOne() first, so a missing cat is already handled, then take the cat out with splice.
Press Run tests to start the app. Its log appears here.Graded endpoints
The service stores the cat and returns it with its id
The same service instance answers, so the second cat gets the next id
Both cats, in the order they were created: the array lives as long as the app
findOne() looks the cat up by id
A cat that does not exist is a 404 from NotFoundException, with Nest's body for it
remove() returns the cat it took out
Tom is gone, so removing him again is the same 404 as looking him up
Only Luna is left