Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Develop With Ai
Structure Business Logic With Di
Upgrade Putnami
Principles
Tooling & Workspace
Workspace
Cli
Jobs & Caching
Extensions
Templates
Error Handling
Frameworks
Typescript
ExtensionOverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart ClientSchema
Go
ExtensionOverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Python
Extension
Platform
Ci
  1. DocsSeparator
  2. How ToSeparator
  3. Structure Business Logic With Di

Structure business logic with DI

You will create a service layer with dependency injection, organize it into a module, and inject it into API endpoints.

Steps

1) Define a service

Services are plain classes. Dependencies come through the constructor:

// src/services/user.service.ts
export class UserService {
  constructor(private db: DatabaseService) {}

  async getUser(id: string) {
    return this.db.query(`SELECT * FROM users WHERE id = $1`, [id]);
  }

  async listUsers() {
    return this.db.query(`SELECT * FROM users`);
  }
}

No decorators, no framework imports — just a class.

2) Register providers

In src/main.ts, register services with explicit dependencies:

import { application, http, api } from '@putnami/application';

export const app = () =>
  application()
    .use(http())
    .use(api())
    .provide(DatabaseService)
    .provide(UserService, { deps: [DatabaseService] });

The deps array tells the DI container what to pass to the constructor, in order.

3) Inject into endpoints

Use .inject() on endpoint(), loader(), or action() builders. The injected dependencies become the first argument of the handler:

// src/api/users/get.ts
import { endpoint } from '@putnami/application';
import { UserService } from '../../services/user.service';

export default endpoint()
  .inject({ users: UserService })
  .handle(async ({ users }) => {
    return { users: await users.listUsers() };
  });
// src/api/users/[id]/get.ts
import { endpoint, Uuid } from '@putnami/application';
import { UserService } from '../../../services/user.service';

export default endpoint()
  .params({ id: Uuid })
  .inject({ users: UserService })
  .handle(async ({ users }, ctx) => {
    const user = await users.getUser(ctx.params.id);
    if (!user) throw new HttpException(404, 'User not found');
    return { user };
  });

4) Group into modules

When your service layer grows, group related providers into modules with explicit contracts:

// src/modules/auth.module.ts
import { module } from '@putnami/application';

export const authModule = module('auth')
  .require(DatabaseService)                              // Must exist in parent
  .provide(AuthService, { deps: [DatabaseService] })     // Public — usable by endpoints
  .provide(TokenValidator, { visibility: 'private' });   // Private — internal to module

Mount the module in your application:

export const app = () =>
  application()
    .use(http())
    .use(api())
    .provide(DatabaseService)
    .use(authModule);

5) Use scoped providers for per-request state

Mark providers as scoped to get a fresh instance per HTTP request:

export const app = () =>
  application()
    .provide(RequestContext, { scope: 'scoped' })
    .provide(AuditLogger, { deps: [RequestContext] });

Scoped providers are automatically resolved per request when injected into endpoints.

6) Test with fork

Fork the DI context and override providers for test isolation:

import { describe, test, expect } from 'bun:test';
import { ContainerContext, provide } from '@putnami/runtime';

describe('UserService', () => {
  test('lists users', async () => {
    await using ctx = new ContainerContext('test');
    ctx.register(provide(DatabaseService, () => ({
      query: async () => [{ id: '1', name: 'Alice' }],
    })));
    ctx.register(provide(UserService, { deps: [DatabaseService] }));
    await ctx.start();

    const users = ctx.get(UserService);
    const result = await users.listUsers();
    expect(result).toHaveLength(1);
  });
});

Next steps

  • See the Dependency Injection reference for scopes, tags, lazy providers, and tracing
  • Add persistence for database-backed services
  • Add authentication to protect injected endpoints

Companion sample: typescript/samples/05-dependency-injection — run putnami serve @example/05-dependency-injection to see DI in action.

You now have a service layer with dependency injection, organized into modules, injected into type-safe endpoints, and testable in isolation.

On this page

  • Structure business logic with DI
  • Steps
  • 1) Define a service
  • 2) Register providers
  • 3) Inject into endpoints
  • 4) Group into modules
  • 5) Use scoped providers for per-request state
  • 6) Test with fork
  • Next steps