Use cases
From a UML perspective, use cases model interactions between users, systems, and an application, outlining specific scenarios and outcomes. In ExpressoTS, use cases encapsulate business logic, clearly separating it from controllers, which handle routing and request management. This keeps the core functionality and business rules distinct from request handling.
While you can use any design pattern, such as MVC, the ExpressoTS opinionated template emphasizes the use of a clean architecture. We believe this approach leads to more maintainable and scalable applications.
Case study
Here is an example of a use case diagram for a project x:

In the use case diagram above, the User actor triggers Login or Registration. Each oval becomes its own @provide() class with an execute() method. The <<include>> relationships become injected providers (for example, Validate credentials calls the external Service through AuthService). The <<extend>> path for Login suspended becomes a domain error thrown from the Login use case.
Use cases should contain business logic only — not HTTP routing, not DTO validation decorators (that belongs on DTO classes), and not direct Express request/response handling.
Create use case
You can create use cases using the ExpressoTS CLI:
expressots g u use-case-name
The examples below implement the Login and Registration flows from the Project X diagram.
Login
Login always validates credentials (<<include>>). A suspended account follows the Login suspended extension path.
import { provide, inject } from "@expressots/core";
import { AuthService } from "../providers/auth.service";
import { LoginUserRequestDTO } from "../dto/login-user.request.dto";
import { LoginUserResponseDTO } from "../dto/login-user.response.dto";
import { LoginSuspendedError } from "../errors/login-suspended.error";
@provide(LoginUserUseCase)
export class LoginUserUseCase {
constructor(@inject(AuthService) private auth: AuthService) {}
async execute(dto: LoginUserRequestDTO): Promise<LoginUserResponseDTO> {
if (await this.auth.isSuspended(dto.email)) {
throw new LoginSuspendedError(dto.email);
}
const authenticated = await this.auth.validateCredentials(
dto.email,
dto.password,
);
return { authenticated };
}
}
AuthService represents the external Service actor in the diagram — credential validation stays outside the use case class but is invoked through DI.
Registration
Registration always sends a verification email (<<include>>).
import { provide, inject } from "@expressots/core";
import { EmailService } from "../providers/email.service";
import { RegisterUserRequestDTO } from "../dto/register-user.request.dto";
import { RegisterUserResponseDTO } from "../dto/register-user.response.dto";
@provide(RegisterUserUseCase)
export class RegisterUserUseCase {
constructor(@inject(EmailService) private email: EmailService) {}
async execute(dto: RegisterUserRequestDTO): Promise<RegisterUserResponseDTO> {
// Persist the user (repository injection omitted for brevity)
const userId = await this.createUser(dto);
await this.email.sendVerification(dto.email);
return { userId, status: "pending_verification" };
}
private async createUser(dto: RegisterUserRequestDTO): Promise<string> {
// ...
return "user-id";
}
}
Explanation
Each use case implements one business scenario from the diagram and exposes a single execute() method. Request and response shapes are defined as DTOs; controllers validate the incoming DTO and call execute().
It is common to inject providers and repositories through the constructor. That keeps Validate credentials and Send verification email as separate, testable collaborators rather than inline logic inside the use case.
Injection
ExpressoTS Dependency Injection lets you inject providers, repositories, and other use cases via constructors or properties.
Constructor injection
import { provide, inject } from "@expressots/core";
import { AuthService } from "../providers/auth.service";
@provide(LoginUserUseCase)
export class LoginUserUseCase {
constructor(@inject(AuthService) private auth: AuthService) {}
async execute(dto: LoginUserRequestDTO): Promise<LoginUserResponseDTO> {
const authenticated = await this.auth.validateCredentials(
dto.email,
dto.password,
);
return { authenticated };
}
}
Property injection
import { provide, inject } from "@expressots/core";
import { EmailService } from "../providers/email.service";
@provide(RegisterUserUseCase)
export class RegisterUserUseCase {
@inject(EmailService) private email!: EmailService;
async execute(dto: RegisterUserRequestDTO): Promise<RegisterUserResponseDTO> {
await this.email.sendVerification(dto.email);
return { status: "pending_verification" };
}
}
It is important to adhere to the principle of single responsibility when implementing use cases. Each use case should only handle a specific business logic, and if you find yourself implementing multiple use cases in a single class, it is time to review your implementation to ensure that each use case has a clear and concise responsibility.
Support the Project
ExpressoTS is MIT-licensed open source. See the support guide to contribute.