Skip to content

Services

Services contain the business logic of the application, orchestrating between controllers and repositories.

Service Structure

Services live in src/services/ and are injected into controllers.

typescript
import { Inject, Singleton } from "typescript-ioc"

@Singleton
export class BookService {
	constructor(
		@Inject private bookRepository: BookRepository,
		@Inject private graphRepository: BookGraphRepository,
	) {}

	async findAll(): Promise<Book[]> {
		return this.bookRepository.findAll()
	}

	async create(data: CreateBookRequest): Promise<Book> {
		const book = await this.bookRepository.create(data)
		await this.graphRepository.createNode(book)
		return book
	}
}

Dependency Injection

Services use typescript-ioc for dependency injection:

typescript
// Mark as singleton for single instance
@Singleton
export class MyService {
	constructor(
		@Inject private repo: MyRepository,
		@Inject private otherService: OtherService,
	) {}
}

Service Responsibilities

Do

  • Business logic and validation
  • Orchestrate multiple repositories
  • Handle transactions
  • Emit domain events
  • Transform data between layers

Don't

  • Handle HTTP concerns (headers, status codes)
  • Access request/response objects
  • Perform direct database queries

Transactions

Use TypeORM's transaction manager for multi-step operations:

typescript
async transferBook(fromUserId: string, toUserId: string, bookId: string) {
	return this.dataSource.transaction(async (manager) => {
		const bookRepo = manager.getRepository(Book)
		const book = await bookRepo.findOneBy({ id: bookId })

		book.ownerId = toUserId
		await bookRepo.save(book)

		// Additional operations in same transaction
	})
}

Error Handling

Throw typed errors that controllers can handle:

typescript
import { NotFoundError, ValidationError } from "../errors"

async findById(id: string): Promise<Book> {
	const book = await this.bookRepository.findById(id)
	if (!book) {
		throw new NotFoundError("Book not found")
	}
	return book
}

Testing Services

Services are the primary unit testing target:

typescript
describe("BookService", () => {
	let service: BookService
	let mockRepository: MockBookRepository

	beforeEach(() => {
		mockRepository = new MockBookRepository()
		service = new BookService(mockRepository)
	})

	it("should find all books", async () => {
		mockRepository.books = [{ id: "1", title: "Test" }]
		const result = await service.findAll()
		expect(result).toHaveLength(1)
	})
})

Built with VitePress