DEV Community

Cover image for How I Structure a Next.js + NestJS Project for AI Coding Agents
Nael M. Awadallah
Nael M. Awadallah

Posted on

How I Structure a Next.js + NestJS Project for AI Coding Agents

An AI coding agent can put a working feature in the wrong place.

The button works. The endpoint returns 200. TypeScript is happy. But the change also introduces a second API client, copies a billing rule into React, and imports another module's private files.

Every local decision looks reasonable. Together, they make the next feature harder.

That is the problem I want my project structure to address: make the intended place for a change easier to find than a plausible alternative.

Here is the structure I would use for a Next.js frontend and a NestJS backend, with a ticket-management feature as the running example. This is an architectural recommendation, not a benchmark claiming that a folder layout makes agents a certain percentage more accurate.

The goal is practical: give both developers and coding agents a clear answer to four questions:

  1. Where does this behavior belong?
  2. Which other parts may it depend on?
  3. What information crosses the API?
  4. How do we verify the change?

1. Start with ownership, then choose folders

Consider a request: show the outstanding balance on a ticket and let an operator record a payment.

Before choosing filenames, I want this ownership map:

Concern Owner
Ticket card, dialog, loading state Frontend ticket feature
HTTP request and query invalidation Frontend ticket API and hooks
Permission to record a payment Backend authorization
Payment validation and persistence Backend payment collection use case
Authoritative balance Backend billing logic
API response shape Explicit response DTO

The frontend may format a balance. It does not become the authority on whether a payment is valid.

This matters because a vague instruction such as “add payment support” leaves architectural decisions hidden inside an implementation task. A small ownership table makes those decisions visible before code is written.

2. Keep the repository map boring

For a monorepo, my starting layout would be:

apps/
  web/
    AGENTS.md
    src/
      app/
      modules/
      components/ui/
      lib/
  api/
    AGENTS.md
    src/
      modules/
      common/
docs/
  architecture.md
  decisions/
AGENTS.md
Enter fullscreen mode Exit fullscreen mode

The root instructions explain how the applications fit together. Application instructions contain commands and conventions specific to that application. A short architecture document explains ownership and dependency direction.

This does not require a monorepo. With separate repositories, I would keep the same internal structure and document the counterpart repository and contract-generation workflow. I would also identify the relevant backend and frontend branches when assigning a task.

Moving two repositories into one is not a prerequisite for using an agent well.

3. In Next.js, organize application code by feature

Next.js supports several organization strategies. Keeping features outside app/ is a convention I choose, rather than a framework requirement.

For a ticket feature:

src/modules/tickets/
  api/
    tickets.api.ts
    ticket-query-keys.ts
  components/
    ticket-card.tsx
    payment-dialog.tsx
  hooks/
    use-record-payment.ts
  pages/
    tickets-page.tsx
  types/
    ticket.types.ts
  index.ts
Enter fullscreen mode Exit fullscreen mode

I create a directory when there is something to put in it. A feature with two files does not need six empty folders.

The route stays small:

// src/app/(dashboard)/tickets/page.tsx
import { TicketsPage } from "@/modules/tickets";

export default function Page() {
  return <TicketsPage />;
}
Enter fullscreen mode Exit fullscreen mode

Route-specific responsibilities can stay in app/: layouts, metadata, route parameters, and route-level data loading. Business UI lives with its feature.

When an agent needs to change the payment dialog, the surrounding API call, hook, and types are nearby. It has a useful starting point without needing a global components/ or hooks/ directory to explain the whole application.

Make public entry points deliberate

My feature entry point exports only what other features need:

// src/modules/tickets/index.ts
export { TicketsPage } from "./pages/tickets-page";
export type { TicketSummary } from "./types/ticket.types";
Enter fullscreen mode Exit fullscreen mode

Other features use that public entry point. Files inside the ticket feature can use relative imports.

I do not export every helper “just in case.” Each export is another dependency the project may need to support later.

There is also a Next.js-specific boundary to preserve: client and server code. A public entry point should not casually mix interactive UI with database access or server-only secrets. When a feature needs both, I use distinct entry points such as client.ts and server.ts, and mark server-only implementation files appropriately.

A barrel file is a navigation convention. It is not a security boundary.

4. In NestJS, distinguish a folder from a module

A larger ticket feature might look like this:

src/modules/tickets/
  tickets.module.ts
  tickets.controller.ts
  tickets.service.ts
  dto/
    ticket-summary.dto.ts
  entities/
    ticket.entity.ts
  payment-collection/
    payment-collection.controller.ts
    payment-collection.service.ts
    dto/
      record-payment.dto.ts
Enter fullscreen mode Exit fullscreen mode

payment-collection/ groups a cohesive use case. It does not automatically require its own Nest module. Its controller and provider can be registered by TicketsModule.

I would introduce another module when it needs a distinct dependency boundary or a responsibility that extends beyond tickets.

For example, chat may grow to own messages, participants, unread counts, and realtime delivery. Tickets can be one context chat refers to without owning the entire chat subsystem.

Nest modules make dependencies explicit through imports, providers, and exports. My default is to export the smallest service another module actually needs. A focused ticket lookup service can be a better dependency than a service exposing every ticket mutation.

I avoid treating direct access to another module's repository as the automatic shortcut. If repository access is intentional, I document who owns the rules that must still apply.

The question is: does this responsibility need its own boundary? File count alone does not answer it.

5. Make the API contract carry the meaning

The frontend should consume a response designed for the use case, rather than importing an ORM entity.

Here is an illustrative response type:

type TicketSummary = {
  id: string;
  currency: string;
  totalMinor: number | null;
  paidMinor: number;
  outstandingMinor: number | null;
};
Enter fullscreen mode Exit fullscreen mode

The contract also needs semantics:

  • Money uses integer minor units for the specified currency.
  • In this example, totalMinor: null means the charge is not finalized.
  • outstandingMinor: null means a final outstanding balance is not available.
  • paidMinor represents confirmed payments; refunds and credits need explicitly defined treatment if supported.
  • The backend determines the authoritative outstanding amount.

Without those definitions, an agent might turn null into zero and display an unfinished charge as “Paid.” The type checker would accept it.

For this example, I would specify the expected responses before implementation:

Case Total Paid Outstanding
Charge still running null 0 null
Final charge, unpaid 6000 0 6000
Partial payment 6000 2000 4000
Fully paid 6000 6000 0

These are example rules, not universal billing rules. A product that supports advance payments needs additional cases.

For a growing project, I would generate frontend types from a maintained OpenAPI schema. NestJS provides OpenAPI tooling, but useful generated types still depend on accurate response schemas.

Generated types can detect a renamed property. They cannot explain what “finalized” means. That belongs in the contract documentation and behavior tests.

6. Give agents instructions they can act on

“Write clean code” leaves almost every decision open.

I want instructions like these instead:

# Project instructions

## Ownership
- apps/web owns presentation and interaction.
- apps/api owns authorization, billing, and persistence.
- Follow an existing nearby implementation before adding a pattern.

## Boundaries
- Import another frontend feature through its public entry point.
- Keep server-only code out of client entry points.
- Keep ORM entities inside the backend.
- Document API changes and regenerate contract types when applicable.

## Scope
- Make the smallest cohesive change that satisfies the task.
- Avoid unrelated renames and dependency changes.
- Surface unresolved business rules before implementing them.

## Verification
- Use the commands in the relevant application's AGENTS.md.
- Report which checks ran, their results, and any blocked checks.
Enter fullscreen mode Exit fullscreen mode

AGENTS.md is an instruction format supported by multiple coding tools. Support and loading behavior vary, so I check the tool's documentation rather than assuming every agent reads every file.

For Claude Code, I would use its documented CLAUDE.md mechanism and import the shared guidance where appropriate:

@AGENTS.md
Enter fullscreen mode Exit fullscreen mode

I would verify that the expected instructions are loaded in the actual working directory. One maintained set of rules is easier to trust than two copies that slowly disagree.

Application instructions should include commands that really exist. If the backend uses npm and Jest, those instructions should say so. Example commands pasted from a pnpm project are worse than no commands when they send the agent down the wrong path.

7. Put business decisions somewhere that survives the chat

Suppose a ticket costs more the longer a car stays parked.

The decision “freeze the charge when retrieval is requested” affects more than one method:

  • The backend needs a finalization point.
  • The API needs to distinguish an ongoing charge from a finalized one.
  • The UI needs different states.
  • Cancellation and a second retrieval request need defined behavior.
  • Tests need to preserve the decision.

I would record the rule in a short decision document:

# Billing finalization

Status: accepted for this example

Decision:
Finalize the charge when retrieval is requested.

Consequences:
- Waiting for retrieval does not increase the finalized charge.
- A repeated request must not finalize the same charge twice.

Unresolved:
- Whether cancellation reopens billing.
- Which pricing policy applies to a later request.
Enter fullscreen mode Exit fullscreen mode

The unresolved section is useful. It gives the agent a reason to ask a specific question instead of silently inventing policy.

Once a decision changes, update both the document and its behavior tests. Otherwise, documentation becomes another competing source of truth.

8. Give each task a path through the structure

With the boundaries defined, a task can be precise without prescribing every implementation detail:

Add an outstanding-balance display to the ticket card.

Read the repository instructions and inspect the existing ticket flow.

Expected behavior:
- Show "Calculating" when outstandingMinor is null.
- Show "Paid" when outstandingMinor is zero.
- Otherwise format outstandingMinor using the ticket currency.

Ownership:
- The backend provides the authoritative balance.
- The frontend formats and displays it.

Scope:
- Follow the existing API client and query conventions.
- Do not change payment collection or billing finalization rules.
- Identify missing contract information before implementing.

Verification:
- Cover null, zero, and positive outstanding balances.
- Run the relevant application checks.
- Summarize changed files, behavior, and remaining limitations.
Enter fullscreen mode Exit fullscreen mode

This gives the agent a destination, boundaries, and acceptance criteria. It still leaves room to inspect the code and choose the smallest appropriate implementation.

9. Enforce the rules that matter

Instructions help an agent choose. Automated checks tell us when the result violates a rule.

Rule Useful enforcement
Cross-feature imports use public entry points ESLint restrictions or dependency-boundary tooling
Client code must not import server-only implementation Server-only markers and framework build checks
Frontend matches the API schema Type generation, type checking, and drift detection
A user cannot access another tenant's ticket Integration or end-to-end authorization tests
Payments remain correct under retries Idempotency and concurrency tests against the database

The exact lint configuration depends on path aliases and repository layout. I would check both aliased and relative imports; blocking one spelling of a forbidden dependency is not enough.

For payment work, a happy-path test is also insufficient. Two simultaneous requests can both pass a naive “remaining balance” check before either writes. That needs a deliberate database strategy and a test that exercises the race.

Passing TypeScript proves something valuable about types. It does not prove authorization, accounting correctness, or concurrency safety.

10. Adopt this one feature at a time

I would not stop delivery to reorganize the entire codebase.

Start with a feature you already need to change:

  1. Identify its owner and existing conventions.
  2. Keep its implementation together.
  3. Define the entry points other features may use.
  4. Clarify the API fields that are easy to misinterpret.
  5. Add concise instructions and enforce the highest-value boundary.

Then review an actual task. Did the agent find the existing API client? Did it preserve the business rule? Did the diff stay focused? Did the checks catch a meaningful failure?

Those observations are more useful than assuming the new layout improved productivity.

The structure is doing its job when a developer can open a feature, understand what it owns, and make a change without rediscovering the architecture.

An AI coding agent benefits from the same clarity.

Which boundary does your coding agent cross most often: feature imports, frontend/backend responsibilities, or business rules?

References

Top comments (0)