cd /news/ai-tools/integrating-elevenlabs-with-next-js-… · home › topics › ai-tools › article
[ARTICLE · art-148499] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

Integrating ElevenLabs with Next.js: Step-by-Step Guide

A developer published a step-by-step guide for integrating ElevenLabs' text-to-speech API into a Next.js application, using a server-side API route to keep the ElevenLabs API key hidden from the browser. The walkthrough covers creating a Next.js app, storing credentials in environment variables, proxying synthesis requests through a Next.js API route that returns raw MP3 audio, and playing the streamed result in a React component.

by read5 min views1 publishedOct 9, 2026

If you’ve ever wanted to add realistic, AI‑generated voice to a web app—whether it’s a podcast generator, an interactive story, or a voice‑enabled chatbot—ElevenLabs is one of the most powerful text‑to‑speech (TTS) engines on the market today. Its neural models produce natural‑sounding speech, support multiple languages, and even let you clone a voice with a few minutes of audio.

Next.js, with its hybrid rendering capabilities and built‑in API routes, makes it incredibly easy to call external services like ElevenLabs from both the client and the server. In this guide we’ll walk through a full‑stack implementation: a simple UI where users type text, hit “Speak”, and hear the audio streamed back instantly.

npx create-next-app@latest my-voice-app) Create a .env.local file at the root of your project and add the following:

ELEVENLABS_API_KEY=your-elevenlabs-api-key
ELEVENLABS_VOICE_ID=your-default-voice-id   # e.g., “EXAVITQu4vr4xnSDxMaL”

Tip: Keep your API key secret—Next.js automatically exposes only variables prefixed with NEXT_PUBLIC_ to the browser. Since we’ll call ElevenLabs from the server, we don’t need to expose it.

Next.js API routes run on the server, perfect for keeping the API key hidden. Create a new file: pages/api/speak.ts.

import type { NextApiRequest, NextApiResponse } from 'next';
import fetch from 'node-fetch';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { text, voiceId } = req.body;
  if (!text) {
    return res.status(400).json({ error: 'Missing text payload' });
  }

  const voice = voiceId || process.env.ELEVENLABS_VOICE_ID;
  const apiKey = process.env.ELEVENLABS_API_KEY;

  try {
    const response = await fetch(
      `https://api.elevenlabs.io/v1/text-to-speech/${voice}`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'xi-api-key': apiKey!,
        },
        body: JSON.stringify({
          text,
          voice_settings: {
            stability: 0.5,
            similarity_boost: 0.75,
          },
        }),
      }
    );

    if (!response.ok) {
      const err = await response.text();
      throw new Error(`ElevenLabs error: ${err}`);
    }

    // The API returns raw audio (mp3) bytes
    const audioBuffer = await response.arrayBuffer();

    // Stream the audio back to the client
    res.setHeader('Content-Type', 'audio/mpeg');
    res.send(Buffer.from(audioBuffer));
  } catch (error: any) {
    console.error(error);
    res.status(500).json({ error: error.message });
  }
}

What’s happening?

POST request with text (the script) and an optional voiceId. Create a component at components/VoiceSynthesizer.tsx:

import { useState, useRef } from 'react';

export default function VoiceSynthesizer() {
  const [text, setText] = useState('');
  const [, set] = useState(false);
  const audioRef = useRef<HTMLAudioElement>(null);

  const handleSpeak = async () => {
    if (!text.trim()) return;
    set(true);

    try {
      const response = await fetch('/api/speak', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text }),
      });

      if (!response.ok) throw new Error('Failed to synthesize');

      const blob = await response.blob();
      const url = URL.createObjectURL(blob);
      audioRef.current!.src = url;
      audioRef.current!.play();
    } catch (err) {
      console.error(err);
      alert('Something went wrong – check the console.');
    } finally {
      set(false);
    }
  };

  return (
    <div style={{ maxWidth: '600px', margin: '2rem auto' }}>
      <textarea
        rows={4}
        placeholder="Enter the script you want spoken..."
        value={text}
        onChange={(e) => setText(e.target.value)}
        style={{ width: '100%', padding: '0.5rem' }}
      />
      <button
        onClick={handleSpeak}
        disabled={}
        style={{
          marginTop: '1rem',
          padding: '0.75rem 1.5rem',
          background: '#0066ff',
          color: '#fff',
          border: 'none',
          cursor:  ? 'not-allowed' : 'pointer',
        }}
      >
        { ? 'Generating…' : 'Speak'}
      </button>

      <audio ref={audioRef} controls style={{ marginTop: '1rem', width: '100%' }} />
    </div>
  );
}

Add the component to your homepage (pages/index.tsx):

import VoiceSynthesizer from '@/components/VoiceSynthesizer';

export default function Home() {
  return (
    <>
      <h1>🗣️ Text‑to‑Speech with ElevenLabs + Next.js</h1>
      <VoiceSynthesizer />
    </>
  );
}

Now, run npm run dev, type a sentence, and hit Speak—you should hear the AI‑generated voice instantly.

ElevenLabs also lets you upload a short audio sample (≈30 seconds) to create a custom voice. Here’s a quick curl example for the cloning endpoint:

curl -X POST "https://api.elevenlabs.io/v1/voices/add" \
  -H "xi-api-key: $ELEVENLABS_API_KEY" \
  -F "name=MyCustomVoice" \
  -F "files=@/path/to/voice_sample.wav"

The response includes a new voice_id. Store that ID and pass it to the /api/speak route:

await fetch('/api/speak', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ text, voiceId: 'YOUR_CLONED_VOICE_ID' })
});

Now you can let users speak with their voice or any brand‑specific voice you’ve trained.

If you anticipate very long paragraphs, you might want to stream the audio in chunks instead of waiting for the entire MP3. ElevenLabs supports chunked responses via the Accept: audio/mpeg header. Here’s a minimal Node.js streaming example inside the API route:

const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voice}`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'xi-api-key': apiKey!,
    Accept: 'audio/mpeg',
  },
  body: JSON.stringify({ text })
});

res.setHeader('Content-Type', 'audio/mpeg');
response.body?.pipe(res); // pipe the streaming body directly

This keeps memory usage low and lets the browser start playing as soon as the first bytes arrive.

Because the API route contains no server‑side dependencies beyond node-fetch (which is already bundled), you can push the repo to GitHub and connect it to Vercel with one click. Vercel automatically injects your environment variables, so the API key stays secure.

Issue Likely Cause Fix
401 Unauthorized Wrong or missing API key Verify ELEVENLABS_API_KEY in.env.local and that it’s set in Vercel
No audio output Content-Type mismatch Ensure you set audio/mpeg on the response and that the<audio> tag receives a valid Blob URL
Slow generation Large text blocks Split the text into sentences and call the API sequentially, then concatenate the audio blobs client‑side

Integrating ElevenLabs with Next.js gives you a production‑ready, server‑secure way to add high‑quality TTS and voice cloning to any web experience. With just a few lines of code you can:

Ready to give your users a voice? Grab your ElevenLabs API key and start building today:

👉 Try ElevenLabs now: https://try.elevenlabs.io/kr07zfuqn1bp

Happy coding, and may your apps speak as smoothly as you type!

── more in #ai-tools 4 stories · sorted by recency
── more on @elevenlabs 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/integrating-elevenla…] indexed:0 read:5min 2026-10-09 · —