Skip to main content
Version: 4.0.0-preview

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:

Project X use case diagram with Login and Registration flows

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.

info

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.

login-user.usecase.ts
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>>).

register-user.usecase.ts
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

Login use case with 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

Registration use case with 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" };
}
}
tip

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.