Practicing Clean Architecture in Next.js
Structure that clearly separates UseCase and Infra boundaries to suppress increase in complexity when expanding functions

Introduction
As the Next.js project grows, it becomes difficult to manage domain rules using only a page-centric structure. Relying on the convenience of the App Router causes business logic to be distributed across Route, Server Action, and components, dramatically increasing change costs. This article presents a structure for practical application of Clean Architecture in the Next.js environment.

Problem definition
There are common signs that architecture quality deteriorates.
- Domain rules are not reused because Server Action directly calls the DB.
- Error handling policies are inconsistent as infrastructure exceptions are exposed directly to the UI.
- Tests exist only on a page-by-page basis, so domain regression cannot be quickly detected.
The key is to align with the framework but maintain a domain-centric dependency orientation. Maintaining boundaries, rather than implementing features, determines long-term speed.
Key concepts
| perspective | Design criteria | Verification points |
|---|---|---|
| Dependency | The inner layer must not know the outer layer | Domain Test Framework Independence |
| Use case | Specify input/output model | Action/Route Reuse Rate |
| adapter | Encapsulating DB/API into ports | Number of files affected when replaced |
| Presentation | UI consumes only the resulting model | Error message consistency |
Clean Architecture is not a method of increasing layers, but of fixing responsibilities. In particular, in Next.js, it is valid to view Route Handler and Server Action as interface adapters.
Code example 1: UseCase + Port definition
export type PublishPostInput = {
actorId: string;
title: string;
body: string;
};
export interface PostRepository {
save(input: { id: string; title: string; body: string; authorId: string }): Promise<void>;
}
export class PublishPostUseCase {
constructor(private readonly repo: PostRepository) {}
async execute(input: PublishPostInput) {
if (input.title.length < 8) throw new Error("title too short");
const id = crypto.randomUUID();
await this.repo.save({ id, title: input.title, body: input.body, authorId: input.actorId });
return { id };
}
}
Code Example 2: Server Action Adapter
"use server";
export async function publishPostAction(formData: FormData) {
const useCase = new PublishPostUseCase(new PostgresPostRepository());
const output = await useCase.execute({
actorId: String(formData.get("actorId")),
title: String(formData.get("title")),
body: String(formData.get("body")),
});
return { ok: true, postId: output.id };
}
Architecture flow
Clean Architecture Layer Structure
In Next.js, the framework code is strong, but domain stability can be ensured by maintaining dependency orientation. By fixing responsibility for each layer as shown below, the scope for regression when adding features can be reduced.
| Layer | responsibility | How to apply |
|---|---|---|
| Entities | domain rules | Post, Policy |
| Use Cases | Business flow | PublishPostUseCase |
| Interface Adapters | Action/Route/Presenter | publishPostAction |
| Frameworks | Next.js, DB, Cache | App Router, Postgres |
Infrastructure diagram
Tradeoffs
- Layer separation increases the initial amount of code, but clearly reduces the scope of impact of changes.
- Having a port interface makes testing easier, but it may feel excessive in small projects.
- Strictly following the structure will stabilize team productivity, but requires learning costs during onboarding.
Cleanup
The purpose of applying Clean Architecture in Next.js is not theoretical purity but operational stability. By placing domain rules at the center and deploying the framework as an adapter, you can achieve both speed and quality of feature expansion.
Image source
- Cover: [source link](https://commons.wikimedia.org/wiki/File:Photocopy_of_blueprint_(See_field_records_for_original_blueprint)
- License: Public domain / Author: Unknown
- Note: After downloading the free license image from Wikimedia Commons, it was optimized to JPG at 1600px.