Confused by NestJS providers? Learn what they are, how @Injectable and dependency injection work, and how to build a CatsService step by step.
Node.js, NestJS, Java Backend Engineer
If you read my last post about controllers, you will remember one rule keep controllers thin. A controller should take the request, pass the real work to someone else and send back the answer.
So how is that "someone else"? In NestJS, it's a provider.
Providers are one of those words that sounds a lot scarier than the idea behind it. By the end of this post you will know what a provider is, what @Injectable() does, how dependency injection works, and how to fix the most common error beginners hit. We will keep building the same cats example, so it should feel familiar.
A provider is a class that NestJS can inject into other classes as a dependency.
That's it. Services, repositories, factories and helpers can all be providers. The main idea is that one class can ask for another, and NestJS runtime wires them together for you. You don't have to create objects with new and pass them around by hand.
In its simplest form, a provider is just a plain JavaScript class that you list in the provides array of a module.
The usual split looks like this:
A service is the most common kind of provider. It's where your application logic lives. Let's make a CatService that stores cats and returns them:
import { Injectable } from '@nestjs/common';import type { Cat } from './interfaces/cat.interface.js';@Injectable()export class CatsService { private readonly cats: Cat[] = []; create(cat: Cat) { this.cats.push(cat); } findAll(): Cat[] { return this.cats; }}It is a basic class with one property (a list of cats) and two methods (create and findAll). The important part is the @Injectable() decorator on top. It attaches a bit of metadata to the class that says "Nest'S container is allowed to manage this one".
And here's the Cat interface the service uses:
export interface Cat { name: string; age: number; breed: string;}Tip: Skip the typing and let the CLI do it: nest g service catsNow we can hand the work over from the controller to the service:
import { Controller, Get, Post, Body } from '@nestjs/common';import { CreateCatDto } from './dto/create-cat.dto.js';import { CatsService } from './cats.service.js';import type { Cat } from './interfaces/cat.interface.js';@Controller('cats')export class CatsController { constructor(private catsService: CatsService) {} @Post() async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); } @Get() async findAll(): Promise<Cat[]> { return this.catsService.findAll(); }}Look at the controller now. It doesn't store anything and it doesn't know how cats are saved. It just calls the service. If you later switch from an in-memory list to a real database, the controller barely changes. That's the payoff of the split.
Notice there is no new CatsService() anywhere. The service arrives through the constructor. That's dependency injection.
Dependancy injection sounds fancy but the idea is simple insted of a class creating the things it needs, it asks for them, and something else hands them over. In Nest, that "something else" is the framework itself.
Nest looks at the type you wrote in the constructor and gives you an instance of that type:
constructor(private catsService: CatsService) {}This one line does two jobs.
private keyword is a TypeScript shortcut called a parameter property. It creates a catsService member on the class and assigns the value for you, so you do not need to write this.catsService = catsService.CatsService type is what Nest uses to find the right provider. TypeScript emits the constructor's parameter types as metadata and Nest's container reads that metadata to decide what to supply.By default, NestJs creates the instance once and shares it with every class that needs it. That is called the singleton scope.
Sooner or later you will see this error at startup. Here's the most common cause.
Because Nest depends on the type existing at runtime the type in your constructor has to be a class. Interfaces and type aliases get erased when TypeScript compiles, so NestJS has nothing to look up.
// This will fail if AppConfig is an interfaceconstructor(private config: AppConfig) {}The same thing happens if you import a class with import type, because that import is erased too. So do not use import type for classes you want Nest to inject.
If you really need to inject something that isn't a class, such as a plain config value, register it under a token and inject it with @Inject(). The custom providers chapter in the official docs covers this.
Two other things worth checking when you see this error:
provides array of a module? (More on the below)@Injectable() on the service class?By default, a provider lives as long as your app does. NestJS creates it when the app starts and destroys it when the app shuts down.
You can change that. For example, a request-scoped provider is created fresh for each incoming request. That is useful in special cases nut you won't need it at the beginning. The official docs cover it in the Injection scopes chapter.
Sometimes a class needs a dependency, but can live without it. A config object is a good example: if it's missing, you fall back to defaults. Mark it with @Optional():
import { Injectable, Optional, Inject } from '@nestjs/common';@Injectable()export class HttpService<T> { constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}}If nothing is registered under the HTTP_OPTIONS token, Nest passes undefined instead of crashing at startup. Your class is then in charge of handling the fallback, usually by merging whatever was injected over a set of defaults.
Everything so far used the constructor. There's another way: put @Inject() straight on a property.
import { Injectable, Inject } from '@nestjs/common';@Injectable()export class HttpService<T> { @Inject('HTTP_OPTIONS') private readonly httpClient: T;}When is this useful? Mainly when a class extends another class. Passing dependencies up through super() in every subclass gets annoying fast. But if your class doesn't extend anything. stick with constructor injection. It makes it obvious what the class needs.
This is the step people forget, just like registering controllers. You have to tell Nest the provider exists by adding it to the providers array in a module:
import { Module } from '@nestjs/common';import { CatsController } from './cats/cats.controller.js';import { CatsService } from './cats/cats.service.js';@Module({ controllers: [CatsController], providers: [CatsService],})export class AppModule {}Now Nest can resolve everything CatsController needs.
Your folder structure at this point looks like this:
src/ cats/ dto/ create-cat.dto.ts interfaces/ cat.interface.ts cats.controller.ts cats.service.ts app.module.ts main.tsMost of the time you won't need to. But if you ever do, there are two options:
ModuleRef to fetch existing instances or create provides on the fly. See the Module reference chapter.bootstrap(), for example to read a config service at startup. See the standalone applications chapter.Running npm run start:dev on your laptop is fine for learning. At some point, though, you'll want your API online where other people can reach it.
Because Nest runs on Node.js, you need a server where you can install Node and keep a process running all the time. That's why a VPS makes more sense here than basic shared hosting, which is usually built for PHP sites and doesn't give you that kind of control.
I use Verpex VPS Hosting for this. Their unmanaged Linux VPS gives you full root access, so you pick the operating system, install the Node version you want, and set up the stack yourself. The plans run on NVMe SSD storage with dedicated resources (so you're not sharing your slice with a noisy neighbour), and there's a 99.9% uptime SLA plus round-the-clock infrastructure support. A small plan is plenty for a first NestJS project, and you can move up later if your traffic grows.
One honest warning: "unmanaged" means exactly that. Updates, firewall rules and security are your job, not theirs. If you've never touched a Linux server before, expect a bit of a learning curve. If you're comfortable with SSH and a terminal, that freedom is the whole point.
A typical first deploy looks like this: connect over SSH, install Node.js, clone your project, run npm install and npm run build, then keep it alive with a process manager like PM2.
Disclosure: this is an affiliate link. If you sign up through it, I may earn a commission at no extra cost to you.
| Concept | What it means |
|---|---|
| Provider | A class Nest can inject into other classes |
| Service | The most common provider; holds your logic |
@Injectable() | Marks a class as manageable by Nest's container |
| Constructor injection | Nest reads the constructor type and supplies the instance |
providers: [] | Where you register providers in a module |
@Optional() | The dependency may be missing; Nest passes undefined |
@Inject('TOKEN') | Inject something that isn't a class, by token |
| Default scope | One shared instance for the whole app |
A class that can be injected as a dependency into controllers or other providers. Services, repositories, factories and helpers can all be providers.
A controller handles incoming requests and sends responses. A service holds the application logic and is injected into the controller.
It attaches metadata to a class so Nest's container can manage it and inject it where needed.
Usually the dependency isn't registered in a module's providers array, or the constructor type isn't a real class (for example an interface, or a class imported with import type).
By default, yes. Nest creates one instance and shares it. You can change this with scopes, such as request-scoped providers.
Constructor injection in almost every case. Property injection is mainly handy when a class extends another class.
Learn how NestJS controllers work. Routes, @Get and @Post, route params, query params, request body, status codes and a full CRUD example.
NestJS is a Node.js framework written in TypeScript that gives your backend a clear structure, so it doesn’t get messy as it grows. Think of it as organized scaffolding on top of Express (the default) or Fastify.