Integrating Claude API with Next.js: A Complete Guide

Learn how to seamlessly add Anthropic's Claude AI capabilities to your Next.js applications for enhanced user experiences and AI-powered features.

Back

Table of Contents

Integrating Claude API with Next.js: A Complete Guide

Learn how to seamlessly integrate Anthropic's Claude AI capabilities into your Next.js applications for enhanced user experiences and AI-powered features.

🤖 Introduction to Claude and Next.js

In today's rapidly evolving tech landscape, AI has become an indispensable tool for developers looking to create more intelligent, responsive applications. Anthropic's Claude, a powerful AI assistant, offers sophisticated natural language processing capabilities that can significantly enhance web applications. When combined with Next.js—React's popular framework for building modern web applications—developers can create cutting-edge AI-powered experiences with relative ease.

This guide will walk you through the process of integrating the Claude API with your Next.js application, from setting up the necessary environment to implementing practical features. Whether you're looking to build a chatbot, analyze documents, or generate content, this integration will provide the foundation you need.

Image suggestion: A visual diagram showing Claude API connecting to a Next.js application, with data flowing between them.


🛠 Setting Up Your Development Environment

Before diving into code, let's ensure you have everything you need to get started with Claude API integration in your Next.js project.

Prerequisites

  • Node.js (version 14.x or later)
  • npm or yarn package manager
  • A Next.js project (existing or new)
  • An Anthropic API key (sign up at Anthropic's developer portal)

Creating a New Next.js Project

If you don't already have a Next.js project, you can create one using the following command:

npx create-next-app@latest claude-integration
cd claude-integration

Installing Required Dependencies

To work with the Claude API, you'll need to install the Anthropic JavaScript SDK:

npm install @anthropic-ai/sdk
# or
yarn add @anthropic-ai/sdk

For better development experience, also consider adding these helpful packages:

npm install dotenv
# or
yarn add dotenv

Setting Up Environment Variables

Create a .env.local file in your project root to securely store your API key:

ANTHROPIC_API_KEY=your_api_key_here

To ensure Next.js can access these environment variables, create or update next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  env: {
    ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
  },
}
 
module.exports = nextConfig

Important note: Never commit your API keys to version control. Make sure .env.local is included in your .gitignore file.


🔌 Basic Claude API Integration

Let's start with a basic integration to understand how Claude API works with Next.js.

Creating an API Route

First, create an API route in your Next.js application to handle Claude API requests. Create a file at pages/api/claude.js:

import Anthropic from '@anthropic-ai/sdk';
 
export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ message: 'Method not allowed' });
  }
 
  try {
    const { prompt } = req.body;
    
    if (!prompt) {
      return res.status(400).json({ message: 'Prompt is required' });
    }
 
    const anthropic = new Anthropic({
      apiKey: process.env.ANTHROPIC_API_KEY,
    });
 
    const completion = await anthropic.completions.create({
      model: 'claude-3-sonnet-20240229',
      max_tokens_to_sample: 1000,
      prompt: `\n\nHuman: ${prompt}\n\nAssistant:`,
    });
 
    return res.status(200).json({ response: completion.completion });
  } catch (error) {
    console.error('Error calling Claude API:', error);
    return res.status(500).json({ message: 'Error processing your request', error: error.message });
  }
}

Creating a Simple Frontend Interface

Now, let's create a simple interface to interact with our Claude API endpoint. Create or modify pages/index.js:

import { useState } from 'react';
import Head from 'next/head';
import styles from '../styles/Home.module.css';
 
export default function Home() {
  const [prompt, setPrompt] = useState('');
  const [response, setResponse] = useState('');
  const [isLoading, setIsLoading] = useState(false);
 
  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsLoading(true);
    
    try {
      const res = await fetch('/api/claude', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ prompt }),
      });
      
      const data = await res.json();
      
      if (res.ok) {
        setResponse(data.response);
      } else {
        setResponse(`Error: ${data.message}`);
      }
    } catch (error) {
      setResponse(`Error: ${error.message}`);
    } finally {
      setIsLoading(false);
    }
  };
 
  return (
    <div className={styles.container}>
      <Head>
        <title>Claude AI Integration</title>
        <meta name="description" content="Next.js application with Claude AI integration" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
 
      <main className={styles.main}>
        <h1 className={styles.title}>Claude AI Integration</h1>
        
        <form onSubmit={handleSubmit} className={styles.form}>
          <textarea
            value={prompt}
            onChange={(e) => setPrompt(e.target.value)}
            placeholder="Enter your prompt here..."
            rows={5}
            className={styles.textarea}
            required
          />
          <button 
            type="submit" 
            className={styles.button}
            disabled={isLoading}
          >
            {isLoading ? 'Processing...' : 'Submit'}
          </button>
        </form>
        
        {response && (
          <div className={styles.response}>
            <h2>Claude's Response:</h2>
            <p>{response}</p>
          </div>
        )}
      </main>
    </div>
  );
}

Image suggestion: Screenshot of the simple UI showing a prompt input area and response display.


🔄 Using Claude's Streaming API for Real-time Responses

One of the most engaging ways to interact with AI is through streaming responses, where text appears incrementally as it's generated. Let's implement streaming with Claude API in Next.js.

Creating a Streaming API Route

Create a new file at pages/api/claude-stream.js:

import Anthropic from '@anthropic-ai/sdk';
 
export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ message: 'Method not allowed' });
  }
 
  const { prompt } = req.body;
  
  if (!prompt) {
    return res.status(400).json({ message: 'Prompt is required' });
  }
 
  // Set up streaming headers
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache, no-transform',
    'Connection': 'keep-alive',
  });
 
  try {
    const anthropic = new Anthropic({
      apiKey: process.env.ANTHROPIC_API_KEY,
    });
 
    const stream = await anthropic.completions.create({
      model: 'claude-3-sonnet-20240229',
      max_tokens_to_sample: 1000,
      prompt: `\n\nHuman: ${prompt}\n\nAssistant:`,
      stream: true,
    });
 
    for await (const completion of stream) {
      // Send each chunk as it arrives
      res.write(`data: ${JSON.stringify({ text: completion.completion })}\n\n`);
    }
    
    // End the stream
    res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
    res.end();
  } catch (error) {
    console.error('Error calling Claude API:', error);
    res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
    res.end();
  }
}

Implementing a Streaming Frontend Component

Now, let's create a component that consumes our streaming API. Create a file at components/ClaudeStream.js:

import { useState, useEffect, useRef } from 'react';
import styles from '../styles/ClaudeStream.module.css';
 
export default function ClaudeStream() {
  const [prompt, setPrompt] = useState('');
  const [response, setResponse] = useState('');
  const [isStreaming, setIsStreaming] = useState(false);
  const eventSourceRef = useRef(null);
 
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // Reset previous response
    setResponse('');
    setIsStreaming(true);
    
    // Close any existing connection
    if (eventSourceRef.current) {
      eventSourceRef.current.close();
    }
    
    try {
      // First, send the prompt to the server
      const res = await fetch('/api/claude-stream', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ prompt }),
      });
      
      if (!res.ok) {
        throw new Error(`Server responded with ${res.status}`);
      }
      
      // Set up event source to receive streaming response
      const reader = res.body.getReader();
      const decoder = new TextDecoder();
      
      while (true) {
        const { done, value } = await reader.read();
        
        if (done) {
          break;
        }
        
        const chunk = decoder.decode(value);
        const lines = chunk.split('\n\n');
        
        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const data = JSON.parse(line.substring(6));
            
            if (data.done) {
              setIsStreaming(false);
              break;
            }
            
            if (data.error) {
              setResponse(prev => prev + `\nError: ${data.error}`);
              setIsStreaming(false);
              break;
            }
            
            if (data.text) {
              setResponse(prev => prev + data.text);
            }
          }
        }
      }
    } catch (error) {
      console.error('Error:', error);
      setResponse(`Error: ${error.message}`);
      setIsStreaming(false);
    }
  };
 
  // Clean up on unmount
  useEffect(() => {
    return () => {
      if (eventSourceRef.current) {
        eventSourceRef.current.close();
      }
    };
  }, []);
 
  return (
    <div className={styles.container}>
      <h2 className={styles.title}>Chat with Claude (Streaming)</h2>
      
      <form onSubmit={handleSubmit} className={styles.form}>
        <textarea
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder="Ask Claude something..."
          rows={4}
          className={styles.textarea}
          disabled={isStreaming}
          required
        />
        <button 
          type="submit" 
          className={styles.button}
          disabled={isStreaming || !prompt.trim()}
        >
          {isStreaming ? 'Streaming...' : 'Send'}
        </button>
      </form>
      
      {(response || isStreaming) && (
        <div className={styles.responseContainer}>
          <h3>Claude's Response:</h3>
          <div className={styles.response}>
            {response}
            {isStreaming && <span className={styles.cursor}>|</span>}
          </div>
        </div>
      )}
    </div>
  );
}

Update your pages/index.js to include this new component:

import ClaudeStream from '../components/ClaudeStream';
// ... other imports
 
export default function Home() {
  // ... existing code
  
  return (
    <div className={styles.container}>
      <Head>
        <title>Claude AI Integration</title>
        <meta name="description" content="Next.js application with Claude AI integration" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
 
      <main className={styles.main}>
        <h1 className={styles.title}>Claude AI Integration</h1>
        
        <ClaudeStream />
        
        {/* You can keep your original non-streaming implementation as well */}
      </main>
    </div>
  );
}

Image suggestion: Animated GIF showing the streaming response in action, with text appearing gradually.


📄 Building a Document Analysis Feature with Claude

One of Claude's powerful capabilities is document analysis. Let's create a feature that allows users to upload documents and get insights from Claude.

Setting Up File Upload

First, install necessary packages for file handling:

npm install formidable
# or
yarn add formidable

Create an API route for document analysis at pages/api/analyze-document.js:

import formidable from 'formidable';
import fs from 'fs';
import Anthropic from '@anthropic-ai/sdk';
 
// Disable body parsing, we'll handle it with formidable
export const config = {
  api: {
    bodyParser: false,
  },
};
 
export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ message: 'Method not allowed' });
  }
 
  try {
    // Parse the form data
    const form = new formidable.IncomingForm();
    
    // Get the file and fields from the form
    const [fields, files] = await new Promise((resolve, reject) => {
      form.parse(req, (err, fields, files) => {
        if (err) reject(err);
        resolve([fields, files]);
      });
    });
    
    // Get the file
    const file = files.document;
    if (!file) {
      return res.status(400).json({ message: 'No document uploaded' });
    }
    
    // Read the file content
    const fileContent = fs.readFileSync(file.filepath, 'utf8');
    
    // Get the analysis question
    const { question } = fields;
    if (!question) {
      return res.status(400).json({ message: 'Analysis question is required' });
    }
    
    // Initialize Anthropic client
    const anthropic = new Anthropic({
      apiKey: process.env.ANTHROPIC_API_KEY,
    });
    
    // Send the document and question to Claude
    const completion = await anthropic.completions.create({
      model: 'claude-3-sonnet-20240229',
      max_tokens_to_sample: 2000,
      prompt: `\n\nHuman: I'm going to share a document with you, and I'd like you to analyze it based on this question: ${question}\n\nHere's the document:\n\n${fileContent}\n\nAssistant:`,
    });
    
    return res.status(200).json({ analysis: completion.completion });
  } catch (error) {
    console.error('Error analyzing document:', error);
    return res.status(500).json({ message: 'Error analyzing document', error: error.message });
  }
}

Written by

Marcus Ruud

At

Mon Nov 13 2023