メインコンテンツまでスキップ

Services

Serviceはビジネスロジックを扱うクラスで、controllerや他のserviceへ注入できます。この関心の分離により、コードのテスタビリティと保守性が高まります。

Defining Services

serviceは@Injectable()でデコレートされたクラスです:

import { Injectable } from '@zeltjs/core';

@Injectable()
export class UserService {
  private users = new Map<string, { id: string; name: string }>();

  findAll() {
    return Array.from(this.users.values());
  }

  findOne(id: string) {
    return this.users.get(id);
  }

  create(name: string) {
    const id = crypto.randomUUID();
    const user = { id, name };
    this.users.set(id, user);
    return user;
  }
}

Dependency Injection

controllerへserviceを注入するにはinject()を使います:

@Controller('/users')
export class UserController {
  constructor(private userService = inject(UserService)) {}

  @Get('/')
  findAll() {
    return { users: this.userService.findAll() };
  }

  @Get('/:id')
  findOne(req = request()) {
    const id = req.pathParam('id');
    const user = this.userService.findOne(id);
    if (!user) {
      throw new Error('User not found');
    }
    return user;
  }

  @Post('/')
  async create(req = request(CreateUserBody)) {
    const body = await req.body();
    return this.userService.create(body.name);
  }
}

Service-to-Service Injection

Serviceは他のserviceを注入できます:

@Injectable()
export class UserService {
  constructor(
    private db = inject(DatabaseService),
    private logger = inject(LoggerService)
  ) {}

  async findAll() {
    this.logger.log('Finding all users');
    return this.db.query('SELECT * FROM users');
  }
}

Singleton Scope

デフォルトでは、serviceはsingletonです — アプリケーションのライフサイクル内で、全ての注入先で同じインスタンスが共有されます。これは次のようなケースに最適です:

  • データベース接続
  • 設定service
  • キャッシュservice
import { Injectable, Env, inject } from '@zeltjs/core';

@Injectable()
export class ConfigService {
  constructor(private env = inject(Env)) {}

  get databaseUrl() {
    return this.env.getString('DATABASE_URL');
  }

  get apiKey() {
    return this.env.getString('API_KEY');
  }
}
ヒント

設定には、inject()と組み合わせた@Configクラスの使用を推奨します。詳細はConfigurationを参照してください。

Testing with Mock Services

singletonパターンによりテストが容易になります — モック実装を渡すことができます:

describe('UserController', () => {
  it('should return all users', async () => {
    const mockUsers = [{ id: '1', name: 'John' }];

    const { target } = await createTestTarget(UserController, {
      overrides: [{ provide: UserService, useValue: { findAll: () => mockUsers } as UserService }],
    });

    const result = target.findAll();

    expect(result).toEqual({ users: mockUsers });
  });
});

Best Practices

  1. Single Responsibility — 各serviceは明確な単一の目的を持つべき
  2. Interface Segregation — serviceのメソッドは焦点を絞り、凝集度を保つ
  3. Dependency Injection — 依存は直接生成せず、常に注入する
  4. Testability — テストで容易にモック化できるようserviceを設計する