Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Chapter 4: Providers & Services
Providers are the backbone of NestJS’s dependency injection system. They enable you to write modular, testable, and maintainable code. The most common provider is a service, which contains business logic and data access code. Think of providers as the “workers” in your application—they do the heavy lifting behind the scenes.
4.1 What is a Provider?
In NestJS, a provider is any class annotated with@Injectable(). Providers can be injected into controllers, other providers, or modules. They can be services, repositories, factories, or even values.
Types of Providers
Services:- Contain business logic
- Most common type of provider
- Stateless (usually)
- Orchestrate data flow
- Abstract data access
- Encapsulate database operations
- Make testing easier
- Can swap implementations
- Create instances dynamically
- Handle complex initialization
- Can be async
- Configuration objects
- Constants
- Third-party library instances
Provider Characteristics
All providers share these characteristics:- Decorated with
@Injectable() - Registered in a module’s
providersarray - Can be injected via constructor
- Managed by NestJS DI container
- Have a lifecycle (singleton, request-scoped, transient)
4.2 The Service Layer
The service layer encapsulates business logic and orchestrates data flow between controllers and repositories. Services should be focused on business rules, not HTTP or database details.Service Responsibilities
Services handle:- Business Logic: Core application rules and workflows
- Data Orchestration: Coordinate between multiple repositories
- Validation: Business-level validation (beyond DTO validation)
- Transformation: Transform data between layers
- Integration: Call external services, APIs, etc.
- HTTP concerns (controllers do this)
- Database queries (repositories do this)
- Request/response formatting (controllers/interceptors do this)
If your app is a hospital, the controller is the receptionist (takes patient information, routes them to the right department), the service is the doctor (diagnoses and prescribes treatment — the actual business logic), and the repository is the medical records room (stores and retrieves patient data). The receptionist never performs surgery, the doctor never files paperwork, and the records clerk never diagnoses patients. When you see a service calling res.status(200).json(...), or a controller running a SQL query, that is the equivalent of a doctor filing paperwork — the system works, but it is doing someone else’s job, and it will cause problems as the hospital scales.
Basic Service Structure
Stateless Services
Services should be stateless when possible:- Easier to test
- Thread-safe
- Can be shared across requests
- No side effects between calls
4.3 Repository Pattern
Repositories abstract data access, making it easy to swap databases or mock for tests. This pattern keeps your business logic decoupled from the database implementation.Why Use Repositories?
Without a repository layer, your services end up littered with database-specific code. Imagine you start with PostgreSQL, then management says “we need to support DynamoDB for this module.” If your service callsthis.dataSource.query('SELECT * FROM users') directly, you are rewriting business logic. If your service calls this.userRepository.findAll(), you only swap the repository implementation — the service never changes.
Benefits:
- Abstraction: Business logic does not depend on database — your service does not know (or care) if it is talking to PostgreSQL, MongoDB, or an in-memory array
- Testability: Easy to mock repositories in tests — just provide a fake object with the same method signatures
- Flexibility: Can swap database implementations without touching services — just register a different class for the same injection token
- Separation of Concerns: Data access logic (query optimization, joins, caching) lives in one place
- Reusability: Repository methods like
findByEmail()orfindActive()can be used by multiple services
Basic Repository
Repository with TypeORM
Repository Interface Pattern
Define interfaces for better abstraction:4.4 Domain-Driven Design (DDD)
For complex applications, consider using Domain-Driven Design principles. DDD helps you model your business domain and organize code around business concepts.DDD Building Blocks
Entities:- Objects with unique identity
- Can change over time
- Example: User, Order, Product
- Immutable objects with no identity
- Defined by their attributes
- Example: Email, Money, Address
- Groups of related entities
- Have a root entity (aggregate root)
- Maintain consistency boundaries
- Business logic that doesn’t fit in entities
- Stateless operations
- Coordinate between multiple entities
- Abstract data access
- Work with aggregates
- Provide domain-friendly interface
| Signal | Action |
|---|---|
| CRUD operations with simple validation | Standard service + repository is enough |
| Business rules span 2-3 entities (e.g., “discount applies if customer is VIP AND order is above threshold”) | Introduce domain services |
| You catch yourself writing the same validation in multiple services | Extract to a value object (e.g., Email, Money) |
| Entity state changes have side effects (send email, update cache) | Introduce domain events |
| Multiple developers argue about where logic belongs | Define aggregate boundaries |
| Your service file exceeds 500 lines | Break into domain services + application service |
| Pattern | Complexity | When to Use | File Count (per feature) |
|---|---|---|---|
| Controller + Service | Low | Simple CRUD, prototypes | 2-3 files |
| Controller + Service + Repository | Medium | Most production apps | 3-4 files |
| Controller + Service + Repository + DDD | High | Complex domains, large teams | 6-10 files |
| Controller + CQRS + Event Sourcing | Very High | Event-driven systems, audit requirements | 10-15 files |
4.5 Dependency Injection in Services
Services can depend on other services, repositories, or configuration providers. Use constructor injection for clarity and testability.Injecting Dependencies
Service Depending on Service
Optional Dependencies
Property Injection (Not Recommended)
- Dependencies are explicit — glancing at the constructor tells you everything the class needs
- Easier to test (you can see all dependencies and provide mocks for each)
- TypeScript can infer types from constructor parameters, so NestJS can auto-resolve them
- Fail fast — if a dependency is missing, the app crashes at startup, not when a user hits a specific route
@Inject() on a field) hides dependencies, making it unclear what a class needs to function.
4.6 Real-World Example: User Registration
Let’s see how services and repositories work together in a real-world scenario:Complete Registration Flow
Supporting Services
4.7 Service Composition
Services can be composed to build complex workflows:Orchestration Service
Orchestration services coordinate multiple services to complete a business workflow. They are the conductors of your application’s orchestra — they do not play any instrument themselves, but they make sure every musician plays at the right time, in the right order.4.8 Error Handling in Services
Services should throw domain-specific exceptions:4.9 Edge Cases in Service Design
Edge Case 1: Fire-and-forget operations that fail silently In the order processing example (section 4.7), we used.catch() for notifications. But what if the notification failure means the user never receives their order confirmation? You need a strategy for “important but non-blocking” operations: write them to a persistent queue (database table, Redis list, or message broker) and process them asynchronously with retries. Pure fire-and-forget with .catch(console.error) is only acceptable for truly non-critical side effects.
Edge Case 2: Service methods that return different shapes
A service method that returns User on success but throws NotFoundException on failure has a clear contract. But what about methods that can return null, undefined, or an empty array? Establish a convention early: return null for “not found” in repository methods and throw NotFoundException in service methods. Never let null propagate to the controller — that results in a 200 response with an empty body, which is confusing for API consumers.
Edge Case 3: Circular service dependencies
AuthService needs UsersService to validate credentials. UsersService needs AuthService to hash passwords on user creation. This is a real circular dependency. The fix is not forwardRef — it is extracting HashService into its own module that both can import. When you see a circular dependency, ask: “What is the shared concern?” Extract it into its own service.
Edge Case 4: Long-running service methods and request timeouts
If a service method takes 30 seconds (generating a report, processing a large file), the HTTP request will likely timeout before it completes. Options: (1) return a 202 Accepted with a job ID immediately, then poll for status; (2) use WebSockets to push the result; (3) use a background job queue like Bull. Never make the client wait for long-running operations synchronously.
4.9 Best Practices
Following best practices ensures your services are maintainable and testable.Keep Services Stateless
Use Repositories for Data Access
Favor Constructor Injection
Write Unit Tests
Use Interfaces for Testability
Avoid Circular Dependencies
Separate Business Logic from Data Access
4.10 Summary
You’ve learned how to structure business logic using providers, services, and repositories: Key Concepts:- Providers: Injectable classes that provide functionality
- Services: Contain business logic and orchestrate workflows
- Repositories: Abstract data access from business logic
- DDD: Domain-Driven Design patterns for complex applications
- Dependency Injection: Services depend on other services/repositories
- Keep services stateless
- Use repositories for all data access
- Favor constructor injection
- Write unit tests for services
- Use interfaces for testability
- Avoid circular dependencies
- Separate business logic from data access
Interview Deep-Dive
What is the repository pattern, and why would you add a repository layer between your service and TypeORM/Prisma instead of calling the ORM directly?
What is the repository pattern, and why would you add a repository layer between your service and TypeORM/Prisma instead of calling the ORM directly?
Strong Answer:
- The repository pattern abstracts data access behind an interface, so your service says
this.userRepository.findByEmail(email)instead ofthis.prisma.user.findUnique({ where: { email } }). The service does not know or care whether the data comes from PostgreSQL, MongoDB, or an in-memory array. - The primary benefit is testability. Without a repository, testing a service means mocking the entire Prisma client or TypeORM repository with all their methods. With a custom repository, you mock a simple interface with 5-6 methods that you control.
- The second benefit is portability. I worked on a project that started with TypeORM and needed to migrate to Prisma after TypeORM’s performance on complex joins became a bottleneck. Because we had a repository layer, the migration was contained: we rewrote 12 repository files, and the 40+ service files never changed.
- The trade-off is indirection. For a simple CRUD app with 5 entities, the repository layer adds boilerplate with little benefit. My rule of thumb: add a repository layer when (1) your service needs to be tested without a database, (2) you might swap ORMs, or (3) your queries are complex enough to warrant encapsulation.
- A common anti-pattern is putting business logic in the repository. The repository should only answer “get me this data” or “save this data.” Deciding whether a user is allowed to update their profile is business logic that belongs in the service.
findWithPosts(userId), so the service never touches the query builder. Additionally, custom repository methods are self-documenting — findActiveByRole('admin') is more readable than a 10-line query builder chain scattered across multiple service methods.Your OrderService depends on PaymentService, InventoryService, NotificationService, and AuditService. A colleague says this is too many dependencies. How would you refactor?
Your OrderService depends on PaymentService, InventoryService, NotificationService, and AuditService. A colleague says this is too many dependencies. How would you refactor?
Strong Answer:
- Four to five constructor dependencies is at the edge. The question is: does the
OrderServicegenuinely need to orchestrate all four concerns, or has it accumulated responsibilities over time? - First, check if any dependencies are cross-cutting concerns.
AuditServiceis a prime candidate — audit logging could be handled by an interceptor instead of explicit service calls. That removes one dependency. NotificationServiceis another candidate for extraction. Sending a confirmation email is a side effect, not core order creation logic. If the email service is down, should the order fail? Usually not. Move it to an event handler:OrderServiceemits anOrderCreatedEvent, andNotificationServicelistens for it asynchronously. This decouples the two and removes another dependency.- After refactoring,
OrderServicehas two dependencies:PaymentServiceandInventoryService. These are genuinely part of the order creation workflow. - The general rule: if your constructor has more than 4-5 dependencies, check whether some are cross-cutting concerns that can be moved to interceptors, event handlers, or middleware. If all are genuinely part of core business logic, the service might be an orchestrator, which is a valid pattern.
RefundPaymentCommand to reverse the charge. In NestJS, implement this with the @nestjs/cqrs event bus — the OrderSaga listens for PaymentSucceededEvent and InventoryReservationFailedEvent, and dispatches compensation commands.Should NestJS services throw HTTP exceptions like NotFoundException, or domain-specific errors? Defend your position.
Should NestJS services throw HTTP exceptions like NotFoundException, or domain-specific errors? Defend your position.
Strong Answer:
- My position: services should throw HTTP exceptions for typical REST APIs, and domain exceptions for complex or multi-transport applications.
- The pragmatic argument for HTTP exceptions: in a typical REST API, the service and HTTP layer are deployed together.
NotFoundExceptionmaps directly to 404. If the service throws a genericError('User not found'), the controller has to catch and convert it — that is boilerplate with no benefit. - The purist argument for domain exceptions: if your service is used by both an HTTP controller and a gRPC handler,
NotFoundExceptionis meaningless in gRPC. A domain exception likeUserNotFoundErrorcan be mapped to 404 in the HTTP exception filter and toNOT_FOUNDin the gRPC filter. - My practical rule: if the application only has an HTTP API and is unlikely to add other transports, throw
HttpExceptionsubclasses directly. If it has multiple transports or uses DDD principles, create domain exception classes and map them in transport-specific filters. - The one thing I would never do is throw raw
Errorobjects. They carry no semantic meaning, and the exception filter has to guess the status code.
DomainException class with a code property (like USER_NOT_FOUND). Subclass it for specific errors. Then create two exception filters: an HttpDomainExceptionFilter mapping codes to HTTP statuses, and a GrpcDomainExceptionFilter mapping to gRPC status codes. Register each on its respective transport. The service throws domain exceptions, and each transport handles the mapping independently.How do you structure a NestJS service layer for a domain with complex business rules, like a loan approval system?
How do you structure a NestJS service layer for a domain with complex business rules, like a loan approval system?
Strong Answer:
- For complex domains, I use Domain-Driven Design principles within NestJS’s service layer. The key distinction is between application services (orchestrate workflows) and domain services (encapsulate business rules that span multiple entities).
- The loan approval system would have a
LoanApplicationService(application service) that orchestrates the workflow: receive application, run credit check, calculate risk score, apply underwriting rules. It injects domain services likeCreditScoringServiceandUnderwritingService. - The domain logic lives in entities and value objects. The
LoanApplicationentity has methods likeapprove(),deny(reason),requestAdditionalDocuments(). These enforce invariants: you cannot approve an application that has not passed underwriting. This is a “rich domain model.” - Value objects like
Money,InterestRate,CreditScoreencapsulate validation.new CreditScore(850)succeeds,new CreditScore(-1)throws. This prevents invalid data from propagating. - The anti-pattern to avoid is an “anemic domain model” where entities are just data containers and all logic is in the service. This leads to 500-line service methods that are hard to test.
new LoanApplication({ amount: 50000, creditScore: new CreditScore(720) })), call domain methods, and assert results. Since entities are plain classes with no DI, they are trivially testable. For domain services, inject mocked repositories. The test reads like a specification: “Given a credit score of 720 and amount of 50,000, when underwriting rules are applied, the application should be approved at 4.5%.”