2 min read

Practicing Clean Architecture in Next.js

Structure that clearly separates UseCase and Infra boundaries to suppress increase in complexity when expanding functions

Practicing Clean Architecture in Next.js thumbnail

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.

Next.js에서 Clean Architecture 실천 커버
Wikimedia Commons 기반 무료 이미지

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

perspectiveDesign criteriaVerification points
DependencyThe inner layer must not know the outer layerDomain Test Framework Independence
Use caseSpecify input/output modelAction/Route Reuse Rate
adapterEncapsulating DB/API into portsNumber of files affected when replaced
PresentationUI consumes only the resulting modelError 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

Mermaid diagram rendering...

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.

LayerresponsibilityHow to apply
Entitiesdomain rulesPost, Policy
Use CasesBusiness flowPublishPostUseCase
Interface AdaptersAction/Route/PresenterpublishPostAction
FrameworksNext.js, DB, CacheApp Router, Postgres

Infrastructure diagram

Mermaid diagram rendering...

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

Comments