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:
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:
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:
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:
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