Skip to main content

Overview

This guide describes two supported project structures for building applications with Contract Kit and Next.js:
  • Layer-Split Architecture – optimized for clarity, strong boundaries, and architectural learning
  • Feature-Split Architecture – optimized for vertical development and feature ownership
Both approaches follow the same core principles:
  • Hexagonal architecture (domain → use-cases → ports → infrastructure)
  • Thin Next.js routing
  • Contract-first APIs
  • Explicit dependency injection via ports and providers
Choose the structure that best fits your current needs. You can migrate between them over time.

Core Principles (Applies to All Structures)

Before choosing a folder layout, it helps to understand the rules that matter most.

1. Dependency Direction

Dependencies always flow inward:
UI / Client

Contracts (HTTP)

Use-Cases (Application)

Domain (Business Rules)
Infrastructure (databases, AI services, email, etc.) lives at the edge and is accessed only through ports.

2. Keep Next.js app/ Thin

The Next.js app/ directory should contain routing only:
  • Route handlers
  • Layouts
  • Pages
All business logic lives in src/.

3. Ports Define Capabilities

Ports describe what the application needs, not how it is implemented. Examples:
  • db.projects.create
  • cache.get
  • ai.generateSummary
Use-cases depend on ports. Providers install or replace implementations.
The layer-split approach organizes code by technical responsibility. This is the clearest way to learn and enforce hexagonal architecture.

Directory Structure

app/                         // Next.js routing only
  api/
    [...ck]/route.ts         // Contract Kit HTTP entrypoint
  (app)/
    projects/
      page.tsx

src/
  domain/                    // Business rules & invariants (pure)
    projects/
      rules.ts
      errors.ts
    tasks/
      rules.ts

  use-cases/                 // Application workflows
    projects/
      createProject.ts
      inviteMember.ts
    tasks/
      createTask.ts
    comments/
      postComment.ts

  ports/                     // Dependency contracts (DI boundary)
    index.ts

  infra/                     // Infrastructure implementations
    db/
      drizzle/
        client.ts
        schema.ts
        types.ts
      queries/
        projects.ts
      ports/
        projects.ts
    ai/
      openai.ts
    notifications/
      resend.ts

  contracts/                 // HTTP transport contracts
    projects.ts
    tasks.ts
    comments.ts

  client/                    // Client-side transport
    api-client.ts
    react-query/
      adapter.ts
      hooks/
        useProject.ts
        useCreateProject.ts
    react-hook-form/
      defaults/
        projects.ts
      hooks/
        useCreateProjectForm.ts

  ui/                        // React UI
    components/
      Button.tsx
      Modal.tsx
    features/
      projects/
        ProjectList.tsx
        ProjectPage.tsx
    layouts/
      AppShell.tsx

  server/                    // App composition
    server.ts
    providers/
      drizzle.ts
      redis.ts
    middleware/

  lib/                       // Pure helpers (safe everywhere)
    ids.ts
    time.ts

What Each Layer Is Responsible For

domain/ — Business Rules

Pure logic that answers questions like:
  • Is this valid?
  • Is this allowed?
No HTTP, no databases, no frameworks.
export function canInviteMember(role: "owner" | "admin" | "member") {
  return role === "owner" || role === "admin";
}

use-cases/ — Application Logic

Each file represents a single user intent:
  • create a project
  • invite a member
  • post a comment
Use-cases:
  • call domain rules
  • orchestrate through ports
  • return plain data
export async function createProject(ctx, input) {
  // domain validation
  // persistence via ports
  // side effects via ports
}
Contract Kit does not require a specific use-case abstraction.
You may use plain functions or helpers like createUseCaseFactory.

ports/ — Dependency Contracts

Ports define capabilities, not implementations.
export const ports = definePorts({
  db: {
    projects: {
      create: async () => {
        throw new Error("Not implemented");
      },
    },
  },
  ai: {
    generateSummary: async () => {
      throw new Error("Not implemented");
    },
  },
});

infra/ — Infrastructure

Concrete implementations of ports.

contracts/ — HTTP Contracts

Transport-layer schemas only. Contracts:
  • define request/response shapes
  • contain no business logic
  • are shared by server and client

client/ — Client Transport

How the UI consumes contracts. Includes:
  • API client
  • React Query hooks and options
  • React Hook Form defaults and helpers

ui/ — React Components

Pure UI:
  • reusable components
  • feature-level screens
  • layouts
UI components never import infrastructure.

server/ — Composition

Where everything is wired together:
  • ports
  • providers
  • middleware
  • context creation

lib/ — Pure Utilities

Small, framework-agnostic helpers:
  • IDs
  • dates
  • strings
If it could be published as a tiny npm package, it probably belongs here.

Feature-Split Architecture (Alternative)

The feature-split approach groups code by business feature, with each feature containing its own layers. This improves locality and vertical development speed while keeping the same architectural rules.

Directory Structure

app/
  api/
    [...ck]/route.ts
  (app)/
    projects/page.tsx

src/
  features/
    projects/
      domain/
        rules.ts
        errors.ts
      use-cases/
        createProject.ts
        inviteMember.ts
      contracts.ts
      client/
        react-query/
          hooks.ts
        react-hook-form/
          hooks.ts
      ui/
        ProjectPage.tsx
        ProjectList.tsx
      infra/
        queries.ts
        port.ts

    tasks/
      domain/
      use-cases/
      contracts.ts
      client/
      ui/
      infra/

  shared/
    ports/
      index.ts
    infra/
      db/
        drizzle/
          client.ts
          schema.ts
      ai/
      notifications/
    client/
      api-client.ts
      react-query/
        adapter.ts
    ui/
      components/
      layouts/
    lib/
      ids.ts
      time.ts

  server/
    server.ts
    providers/

Rules That Keep Feature-Split Clean

  • Features may depend on shared/
  • shared/ must never depend on features
  • Use-cases never import infra directly
  • Contracts remain pure

Choosing Between Approaches

CriteriaLayer-SplitFeature-Split
Team sizeSmallMedium–Large
Learning hexagonal architectureExcellentGood
Feature localityLowHigh
Architectural enforcementStrongModerate
Migration to microservicesModerateEasy

Recommendation

  • Start with Layer-Split
  • Migrate to Feature-Split when feature count and team size grow

Quick Decision Guide

If the file…Put it in…
Contains business rulesdomain/
Orchestrates a workflowuse-cases/
Defines HTTP schemascontracts/
Calls external systemsinfra/
Wires ports/providersserver/
Fetches data in Reactclient/
Renders UIui/
Is a pure helperlib/

Summary

Contract Kit supports multiple project structures. The most important things are:
  • clear dependency direction
  • thin routing
  • explicit ports and providers
  • separation between business logic and infrastructure
Choose the structure that helps your team move fastest without breaking these rules.