Back to blog
·6 min read

How to Structure Next.js Projects for AI Integration Without Breaking Your Codebase

Adding AI features to Next.js apps often leads to scattered API calls and unmaintainable code. Here's the exact project structure pattern that keeps your codebase clean as you scale LLM integrations.

The Problem With Adding AI to Existing Next.js Apps

You've built a solid Next.js application. Users are happy. Now you need to add AI—maybe a smart chat feature, automated summaries, or content generation. But where do you put it?

Most teams make the same mistake: they scatter LLM API calls everywhere. A route handler here, a server action there, some utilities in a helpers folder. Within three months, when you need to update your prompt or switch models, you're hunting through 15 files to find every Claude API call. Your codebase becomes a maintenance nightmare.

The real problem isn't that AI is complicated. It's that developers try to bolt AI onto existing Next.js architecture without creating a deliberate structure for it. This leads to technical debt that compounds fast.

Why Your Current Project Structure Is Struggling

If you're working with a standard Next.js app, your directory probably looks like this:

```

app/

├── dashboard/

├── api/

│ └── route.ts

├── components/

└── lib/

├── utils.ts

├── db.ts

└── auth.ts

```

This structure works fine for CRUD applications. But the moment you add AI, you need a place for:

  • API keys and model configuration
  • Prompt templates (they change constantly)
  • Streaming handlers (different from regular API routes)
  • Agent logic and tool definitions
  • Prompt versioning
  • Without a dedicated space for these, they end up scattered in lib/, inside components, or buried in utility files. When your PM says "use GPT-4 Turbo instead of Claude," you've got a two-hour refactor ahead.

    The Winning Structure: Add an /agents Directory

    The cleanest approach is to add an /agents directory at the root of your project. You don't need to refactor anything existing. This incremental approach avoids the "AI rewrite" trap where adding AI features requires restructuring half your codebase.

    Here's the structure:

    ```

    project-root/

    ├── app/

    │ ├── api/

    │ │ └── ai/

    │ │ ├── stream/

    │ │ │ └── route.ts

    │ │ └── complete/

    │ │ └── route.ts

    │ ├── dashboard/

    │ └── components/

    ├── agents/

    │ ├── prompts/

    │ │ ├── summarize.md

    │ │ ├── classify.md

    │ │ └── generate-copy.md

    │ ├── tools/

    │ │ ├── search.ts

    │ │ └── database.ts

    │ ├── models.ts

    │ └── config.ts

    ├── lib/

    └── package.json

    ```

    Let's break down what lives where and why.

    The /agents Directory: Your AI Hub

    Your agents directory is where all LLM-related logic lives. Think of it as the AI layer of your application.

    ### /agents/prompts

    Store your prompts as markdown files. This matters more than you'd think.

    Prompts change. A lot. If they're embedded in TypeScript strings, you end up with merge conflicts and redeployments for what should be configuration changes. Markdown files mean non-engineers can update prompts without touching code.

    Example: agents/prompts/summarize.md

    ```

    You are an expert content summarizer.

    Your task: Create a concise summary of the provided text.

    Rules:

  • Keep summaries under 150 words
  • Preserve key facts and dates
  • Use bullet points for lists
  • Maintain the original tone
  • Text to summarize:

    {text}

    ```

    Then load this in your code:

    ```typescript

    // agents/models.ts

    import fs from 'fs';

    import path from 'path';

    export async function loadPrompt(name: string): Promise<string> {

    const promptPath = path.join(process.cwd(), 'agents', 'prompts', `${name}.md`);

    return fs.promises.readFile(promptPath, 'utf-8');

    }

    ```

    ### /agents/models.ts

    This file initializes your AI model and exposes a consistent interface.

    ```typescript

    import Anthropic from '@anthropic-ai/sdk';

    export const anthropic = new Anthropic({

    apiKey: process.env.ANTHROPIC_API_KEY,

    });

    export async function streamCompletion(

    prompt: string,

    systemPrompt: string,

    maxTokens: number = 1000

    ) {

    return anthropic.messages.stream({

    model: 'claude-3-5-sonnet-20241022',

    max_tokens: maxTokens,

    system: systemPrompt,

    messages: [

    {

    role: 'user',

    content: prompt,

    },

    ],

    });

    }

    ```

    By centralizing model initialization, swapping between Claude and OpenAI later takes one edit instead of ten.

    ### /agents/config.ts

    Store model names, temperature settings, and other tunable parameters here.

    ```typescript

    export const AI_CONFIG = {

    summarization: {

    model: 'claude-3-5-sonnet-20241022',

    maxTokens: 500,

    temperature: 0.3,

    },

    creative: {

    model: 'claude-3-5-sonnet-20241022',

    maxTokens: 1500,

    temperature: 0.8,

    },

    classification: {

    model: 'claude-3-5-sonnet-20241022',

    maxTokens: 100,

    temperature: 0.1,

    },

    };

    ```

    This decouples your prompts from your model choices. Update temperature without touching your route handlers.

    Wire AI Into Your Routes

    Now create streaming route handlers. Next.js 13+ Server Components handle streaming beautifully.

    Create app/api/ai/stream/route.ts:

    ```typescript

    import { streamCompletion } from '@/agents/models';

    import { loadPrompt } from '@/agents/models';

    import { AI_CONFIG } from '@/agents/config';

    export async function POST(request: Request) {

    const { text, promptName } = await request.json();

    const systemPrompt = await loadPrompt(promptName);

    const config = AI_CONFIG[promptName as keyof typeof AI_CONFIG];

    const stream = await streamCompletion(

    text,

    systemPrompt,

    config.maxTokens

    );

    return new Response(stream.toReadableStream(), {

    headers: {

    'Content-Type': 'text/event-stream',

    'Cache-Control': 'no-cache',

    },

    });

    }

    ```

    From your Client Component, fetch this endpoint and stream the response:

    ```typescript

    async function summarizeText(text: string) {

    const response = await fetch('/api/ai/stream', {

    method: 'POST',

    body: JSON.stringify({ text, promptName: 'summarize' }),

    });

    if (!response.body) return;

    const reader = response.body.getReader();

    const decoder = new TextDecoder();

    while (true) {

    const { done, value } = await reader.read();

    if (done) break;

    console.log(decoder.decode(value));

    }

    }

    ```

    Why This Structure Scales

    As your application grows, this structure handles complexity:

  • Add a new AI feature? Create a new prompt file and one route handler.
  • Switch models? Update config.ts and models.ts.
  • Version prompts? Drop v2.md next to summarize.md.
  • Add tools or function calling? Create /agents/tools/ and import in your route handlers.
  • You avoid the refactor trap. Your existing app doesn't change. AI lives in its own layer.

    Common Mistakes This Prevents

    Developers who skip this structure typically hit these walls:

  • Prompts embedded in code—can't iterate without redeploys
  • Model initialization everywhere—switching providers becomes a 10-file edit
  • API keys scattered in environment variables—no clear config layer
  • Route handlers doing everything—they become 200+ lines of unmaintainable logic
  • No separation between LLM calls and application logic—debugging becomes impossible
  • This structure isolates concerns. Your route handlers are thin. Your prompts are versioned. Your model choices are configurable.

    Next: Automate This Setup

    Building this structure manually takes an hour. Doing it consistently across projects takes much longer. Tools like ZipBuild scaffold this exact pattern automatically—generating production-ready Next.js projects with the AI layer already structured and ready to use. But even if you build it yourself, following this architecture means your codebase stays maintainable as you layer in more AI features.

    The key insight: AI integration isn't a feature you bolt on. It's an architectural decision. Make it early, make it deliberate, and your team will thank you when the third AI requirement comes in and takes two hours instead of two days.

    Try the free discovery chat at zipbuild.dev to see how to automate this setup for your next project.

    Written by ZipBuild Team

    Ready to build with structure?

    Try the free discovery chat and see how ZipBuild architects your idea.

    Start Building