Back to blog
·5 min read

How to Add AI Features to Next.js Without Rebuilding Your Architecture

Adding AI to your Next.js app shouldn't mean rewriting everything. We'll show you the architectural pattern that scales from a simple chat interface to complex autonomous agent workflows.

The AI Integration Problem Most Developers Face

You've got a working Next.js application. It's stable, customers are using it, and your codebase is organized. Then comes the request: add AI features.

Most developers panic at this point because they've seen it done wrong before. LLM API calls scattered across route handlers. Prompts hardcoded in components. Model switching requiring changes in five different files. Token limits hit silently because there's no observability layer. The team ends up rebuilding half the codebase just to add what should be a feature.

This doesn't have to happen.

The real issue isn't that AI is complicated. It's that developers add AI features reactively, without a clear architectural boundary. They treat it like any other API integration, which fails the moment you need to version prompts, swap models, add logging, or handle streaming responses properly.

There's a better way: the /agents directory pattern.

Why the /agents Directory Pattern Works

The /agents directory creates a single, isolated layer for all AI capabilities. Think of it like how you might organize database queries in a /lib/db directory, or API calls in a /lib/api directory. By containing AI logic in one place, you get several immediate benefits:

  • Prompt versioning becomes straightforward. Keep old prompts, iterate new ones, switch between versions without touching application code.
  • Model switching takes minutes instead of days. Swap from Claude to GPT or Gemini by changing one configuration file.
  • Observability and monitoring are built in from day one. Every AI call flows through the same interface.
  • Testing is isolated. You can mock AI responses without touching your components or server actions.
  • New team members understand where AI logic lives. No detective work required.
  • The pattern scales from your first simple prompt to complex multi-agent workflows. It works whether you're building a chat interface, document processing pipeline, or autonomous agent system.

    Building Your First /agents Directory

    Start small. Don't try to architect perfection on day one. Here's the minimal structure that handles 80 percent of real-world use cases:

    ```

    /app

    /agents

    /prompts

    chat.ts

    document-analyzer.ts

    /actions

    streamChat.ts

    analyzeDocument.ts

    config.ts

    types.ts

    /api

    /ai

    /stream

    route.ts

    ```

    In /agents/config.ts, set up your model configuration:

    ```

    export const AI_CONFIG = {

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

    temperature: 0.7,

    maxTokens: 2048,

    };

    export const getAnthropicClient = () => {

    return new Anthropic({

    apiKey: process.env.ANTHROPIC_API_KEY,

    });

    };

    ```

    In /agents/prompts/chat.ts, define your first prompt as a reusable function:

    ```

    export const getChatSystemPrompt = (context?: string) => {

    return `You are a helpful assistant.${

    context ? ` Context: ${context}` : ''

    }

    Keep responses concise and actionable.`;

    };

    export const CHAT_PROMPT_VERSION = '1.0.0';

    ```

    This approach means you can update your prompt without touching any route handlers or components. You also have a clear version history if you need to roll back.

    In /agents/actions/streamChat.ts, create a server action that handles the AI call:

    ```

    'use server';

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

    import { AI_CONFIG, getAnthropicClient } from '../config';

    import { getChatSystemPrompt } from '../prompts/chat';

    export async function streamChatResponse(userMessage: string) {

    const client = getAnthropicClient();

    return await client.messages.create({

    model: AI_CONFIG.model,

    max_tokens: AI_CONFIG.maxTokens,

    system: getChatSystemPrompt(),

    messages: [

    {

    role: 'user',

    content: userMessage,

    },

    ],

    stream: true,

    });

    }

    ```

    Then create your API route at /app/api/ai/stream/route.ts to expose this as an HTTP endpoint:

    ```

    import { streamChatResponse } from '@/app/agents/actions/streamChat';

    export async function POST(request: Request) {

    const { message } = await request.json();

    const stream = await streamChatResponse(message);

    return new Response(stream.toReadableStream(), {

    headers: {

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

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

    },

    });

    }

    ```

    Now your component can call this endpoint cleanly, and your AI logic is completely separated from your UI:

    ```

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

    method: 'POST',

    body: JSON.stringify({ message: userInput }),

    });

    ```

    Scaling Beyond the First Prompt

    The pattern stays clean as you grow. Add a second prompt? Drop it in /agents/prompts/. Need to analyze documents instead of chat? Create /agents/prompts/document-analyzer.ts and /agents/actions/analyzeDocument.ts.

    Three months in, when your product team wants to A/B test two different system prompts? Update your config to track active prompt versions, run both in parallel, and log which version gave better results. Your component code doesn't change.

    Six months in, when you need to add observability because your token costs are rising? Add a logging middleware to your server actions. One change, applies everywhere.

    Common Mistakes to Avoid

    Don't mix prompt logic with component logic. If your React component has the system prompt hardcoded, you're doing it wrong.

    Don't create multiple AI clients. Use a single getAnthropicClient function and inject it into your actions.

    Don't skip error handling in your route handlers. Add try-catch blocks and proper error responses so your frontend knows when something failed.

    Don't commit your API keys. Use environment variables, and use .env.local for local development.

    The Bigger Picture

    This pattern works because it acknowledges a fundamental truth: AI capabilities are not the same as regular features. They need versioning, observability, and flexibility built in from the start.

    If you're building a production SaaS application and planning multiple AI features, consider using ZipBuild to scaffold this structure plus authentication, database migrations, and deployment configuration. It saves weeks of architectural decisions.

    But even without scaffolding tools, this /agents pattern is straightforward enough that any Next.js developer can implement it in an hour.

    Next Steps

    Create your /agents directory today. Write your first prompt. Wire it into a route handler. Test it works end-to-end. Commit that checkpoint.

    Then add your second feature to the same structure. You'll immediately see why separating AI logic makes scaling easier.

    Try the free discovery chat at zipbuild.dev to explore how this pattern fits into a complete SaaS stack.

    Written by ZipBuild Team

    Ready to build with structure?

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

    Start Building