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