Back to Blog
Tutorials 15 min read February 10, 2025

Next.js 14 + AI: Building Intelligent Web Applications

Combine the power of Next.js App Router with AI capabilities. Learn streaming responses, Server Actions, and building production-ready AI-powered features.

DevForge Team

DevForge Team

AI Development Educators

Modern web application interface with AI chat feature

Why Next.js + AI is the Perfect Stack

Next.js 14 with the App Router is arguably the best framework for building AI-powered web applications today. Here's why:

Server Components — AI API calls belong on the server. Server Components let you call Claude directly in your components without extra API routes, without exposing your API key, and without client-server round trips.

Streaming — AI responses are streamed token by token. Next.js has excellent built-in support for streaming from both Server Components and API Routes.

Server Actions — Form submissions and mutations that call Claude without needing to build a full API layer.

Edge Runtime — Some AI features benefit from running closer to users. Next.js makes deploying to Vercel's edge network trivial.

In this tutorial, we'll build an AI-powered coding assistant that includes:

  1. Streaming chat interface
  2. Code generation with syntax highlighting
  3. Conversation history
  4. Context-aware responses (the AI knows what code the user is viewing)

Project Setup

bash
npx create-next-app@latest ai-coding-assistant --typescript --tailwind --app
cd ai-coding-assistant
npm install @anthropic-ai/sdk ai

The ai package from Vercel makes streaming much easier — we'll use it.

Set up environment variables in .env.local:

text
ANTHROPIC_API_KEY=sk-ant-...

Building the Streaming API Route

typescript
// app/api/chat/route.ts
import Anthropic from "@anthropic-ai/sdk";
import { NextRequest } from "next/server";

const client = new Anthropic();

export async function POST(req: NextRequest) {
  const { messages, systemPrompt } = await req.json();

  const stream = await client.messages.stream({
    model: "claude-opus-4-5",
    max_tokens: 4096,
    system: systemPrompt || "You are a helpful coding assistant.",
    messages,
  });

  const readableStream = new ReadableStream({
    async start(controller) {
      for await (const chunk of stream) {
        if (
          chunk.type === "content_block_delta" &&
          chunk.delta.type === "text_delta"
        ) {
          const text = chunk.delta.text;
          controller.enqueue(new TextEncoder().encode(text));
        }
      }
      controller.close();
    },
  });

  return new Response(readableStream, {
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
      "Transfer-Encoding": "chunked",
    },
  });
}

The Chat UI Component

typescript
// components/ChatInterface.tsx
"use client";

import { useState, useRef, useEffect } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";

interface Message {
  role: "user" | "assistant";
  content: string;
}

export function ChatInterface({ contextCode }: { contextCode?: string }) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [isStreaming, setIsStreaming] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  const systemPrompt = `You are an expert coding assistant integrated into DevForge Academy.
You help developers understand and write code.

${contextCode ? `The user is currently viewing this code:
\`\`\`
${contextCode}
\`\`\`

Reference this code in your responses when relevant.` : ""}

Format code blocks with appropriate language tags.
Be concise but thorough. Explain your reasoning.`;

  async function sendMessage() {
    if (!input.trim() || isStreaming) return;

    const userMessage: Message = { role: "user", content: input };
    const updatedMessages = [...messages, userMessage];
    setMessages(updatedMessages);
    setInput("");
    setIsStreaming(true);

    // Add empty assistant message to stream into
    setMessages((prev) => [
      ...prev,
      { role: "assistant", content: "" },
    ]);

    try {
      const response = await fetch("/api/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          messages: updatedMessages,
          systemPrompt,
        }),
      });

      const reader = response.body?.getReader();
      const decoder = new TextDecoder();

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

        const text = decoder.decode(value, { stream: true });
        setMessages((prev) => {
          const updated = [...prev];
          updated[updated.length - 1] = {
            role: "assistant",
            content: updated[updated.length - 1].content + text,
          };
          return updated;
        });
      }
    } finally {
      setIsStreaming(false);
    }
  }

  return (
    <div className="flex flex-col h-full">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((msg, i) => (
          <MessageBubble key={i} message={msg} />
        ))}
        {isStreaming && (
          <div className="flex gap-1">
            <span className="animate-bounce">●</span>
            <span className="animate-bounce delay-100">●</span>
            <span className="animate-bounce delay-200">●</span>
          </div>
        )}
        <div ref={messagesEndRef} />
      </div>

      <div className="border-t p-4">
        <div className="flex gap-2">
          <textarea
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === "Enter" && !e.shiftKey) {
                e.preventDefault();
                sendMessage();
              }
            }}
            placeholder="Ask about code, get explanations, generate functions..."
            className="flex-1 resize-none rounded-lg border p-3 text-sm"
            rows={3}
          />
          <button
            onClick={sendMessage}
            disabled={isStreaming || !input.trim()}
            className="px-4 rounded-lg bg-violet-600 text-white disabled:opacity-50"
          >
            Send
          </button>
        </div>
      </div>
    </div>
  );
}

Server Actions for AI Features

Server Actions let you call AI directly from forms:

typescript
// app/actions/explain.ts
"use server";

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

const client = new Anthropic();

export async function explainCode(code: string): Promise<string> {
  const message = await client.messages.create({
    model: "claude-haiku-3-5", // Use cheaper model for simple tasks
    max_tokens: 1024,
    messages: [{
      role: "user",
      content: `Explain this code in 3-5 sentences, focusing on what it does and any notable patterns:

\`\`\`
${code}
\`\`\``
    }]
  });

  return message.content[0].type === "text"
    ? message.content[0].text
    : "";
}

// Use in a component:
// import { explainCode } from "@/app/actions/explain";
// const explanation = await explainCode(selectedCode);

Implementing Context-Aware AI

The key to great AI coding assistants is context. The more you know about what the user is doing, the better the responses:

typescript
// Build rich context for the AI
function buildSystemPrompt(context: {
  currentFile?: string;
  language?: string;
  selectedCode?: string;
  errorMessage?: string;
  userStack?: string[];
}) {
  const lines = [
    "You are an expert coding assistant integrated into a code editor.",
    "",
    "Current context:",
  ];

  if (context.currentFile) {
    lines.push(`- File: ${context.currentFile}`);
  }
  if (context.language) {
    lines.push(`- Language: ${context.language}`);
  }
  if (context.userStack?.length) {
    lines.push(`- Tech stack: ${context.userStack.join(", ")}`);
  }
  if (context.selectedCode) {
    lines.push("", "Selected code:", "```", context.selectedCode, "```");
  }
  if (context.errorMessage) {
    lines.push("", `Current error: ${context.errorMessage}`);
  }

  lines.push(
    "",
    "Provide precise, actionable responses.",
    "Format code blocks with language tags.",
    "Be concise unless the user asks for detail."
  );

  return lines.join("\n");
}

Performance Optimizations

1. Model Selection

Match model to task complexity:

  • claude-haiku-3-5: Simple explanations, quick fixes (fast, cheap)
  • claude-opus-4-5: Complex architecture, code review (slower, more capable)

2. Caching with React cache()

typescript
import { cache } from "react";

export const getExplanation = cache(async (codeHash: string) => {
  // This will only run once per request even if called multiple times
  return fetchExplanation(codeHash);
});

3. Abort Controller for Cancellation

typescript
const controller = new AbortController();

const response = await fetch("/api/chat", {
  method: "POST",
  signal: controller.signal,
  body: JSON.stringify({ messages }),
});

// Cancel if user clicks stop:
cancelButton.onclick = () => controller.abort();

Deploying to Vercel

Next.js + Vercel + Anthropic is a natural combination:

bash
vercel deploy

Add your environment variables in the Vercel dashboard. The streaming will work automatically — Vercel supports streaming responses natively.

Set appropriate function timeouts for long AI responses:

json
// vercel.json
{
  "functions": {
    "app/api/chat/route.ts": {
      "maxDuration": 60
    }
  }
}

What You've Built

You now have the foundation for a production AI coding assistant:

  • Streaming chat interface
  • System prompt with code context
  • Server Actions for simple AI tasks
  • Context-aware responses

From here, you can add: RAG over your codebase, voice input, multi-file editing, automated testing generation, and much more. The architecture scales to any complexity.

#Next.js#AI#React#Claude API#Streaming