DEV Community

VoiceDeveloper
VoiceDeveloper

Posted on

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

Why ElevenLabs and Next.js Make a Perfect Pair

If you’ve ever wanted to turn dynamic text into natural‑sounding speech right inside a modern web app, you’re in the right place. ElevenLabs (https://try.elevenlabs.io/kr07zfuqn1bp) offers a state‑of‑the‑art text‑to‑speech (TTS) API with voice cloning, low latency, and a generous free tier. Coupled with Next.js’s API routes and React rendering model, you can add voice capabilities to anything from a blog post reader to an interactive chatbot without leaving the familiar JavaScript ecosystem.

In this guide we’ll:

  1. Set up a fresh Next.js project.
  2. Securely store your ElevenLabs API key.
  3. Build a server‑side API route that talks to ElevenLabs.
  4. Consume that endpoint from the client, play back audio, and optionally upload a custom voice.

Let’s dive in!


1. Create the Next.js Boilerplate

If you already have a Next.js app, skip to step 2. Otherwise, open a terminal and run:

npx create-next-app@latest elevenlabs-demo
cd elevenlabs-demo
Enter fullscreen mode Exit fullscreen mode

Make sure you’re on at least Next 13 (the app/ directory is optional, but the API routes work the same in both pages and app).


2. Get Your ElevenLabs API Key

  1. Sign up (or log in) at the ElevenLabs portal: https://try.elevenlabs.io/kr07zfuqn1bp.
  2. Navigate to API → Keys and click Create new key.
  3. Copy the key – you’ll need it in the next step.

Security tip: Never commit your key to Git. Use environment variables instead.

Create a .env.local file at the root of your project:

ELEVENLABS_API_KEY=your_secret_key_here
Enter fullscreen mode Exit fullscreen mode

Next.js automatically injects any variable that starts with NEXT_PUBLIC_ to the client, so keep the key server‑only (no NEXT_PUBLIC_ prefix).


3. Build the Server‑Side Proxy

We’ll expose a tiny API route that forwards the request to ElevenLabs. This keeps your secret key hidden and lets you add extra validation later.

Create pages/api/tts.js (or app/api/tts/route.js if you’re using the new app router). Here’s the pages version:

// pages/api/tts.js
import fetch from 'node-fetch';

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

  const { text, voice_id = 'EXAVITQu4vr4xnSDxMaL' } = req.body; // default voice
  if (!text) {
    return res.status(400).json({ error: 'Missing "text" in body' });
  }

  try {
    const elevenResponse = await fetch(
      `https://api.elevenlabs.io/v1/text-to-speech/${voice_id}`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'xi-api-key': process.env.ELEVENLABS_API_KEY,
        },
        body: JSON.stringify({
          text,
          // optional: voice settings, stability, etc.
          voice_settings: { stability: 0.75, similarity_boost: 0.85 },
        }),
      }
    );

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

    // ElevenLabs streams raw audio (mp3). We'll pipe it directly.
    const audioBuffer = await elevenResponse.arrayBuffer();
    res.setHeader('Content-Type', 'audio/mpeg');
    res.send(Buffer.from(audioBuffer));
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: error.message });
  }
}
Enter fullscreen mode Exit fullscreen mode

What’s happening?

  • We only accept POST requests with a JSON body containing text.
  • The route forwards the request to ElevenLabs, using the secret key from process.env.
  • The response is an MP3 binary stream, which we return with the correct MIME type.

4. Hook Up the Frontend

Now we need a UI that lets a user type something, hit “Speak”, and hear the result.

Create a component at components/VoicePlayer.jsx:

// components/VoicePlayer.jsx
import { useState } from 'react';

export default function VoicePlayer() {
  const [text, setText] = useState('');
  const [audioUrl, setAudioUrl] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const speak = async () => {
    setLoading(true);
    setError('');
    try {
      const res = await fetch('/api/tts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text }),
      });

      if (!res.ok) {
        const err = await res.json();
        throw new Error(err.error || 'Failed to fetch audio');
      }

      // Convert the binary response into a Blob URL for the <audio> tag
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      setAudioUrl(url);
    } catch (e) {
      setError(e.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div style={{ maxWidth: '600px', margin: 'auto' }}>
      <textarea
        rows={4}
        placeholder="Enter text to speak..."
        value={text}
        onChange={(e) => setText(e.target.value)}
        style={{ width: '100%', padding: '8px' }}
      />
      <button
        onClick={speak}
        disabled={loading || !text.trim()}
        style={{ marginTop: '8px', padding: '8px 16px' }}
      >
        {loading ? 'Generating...' : 'Speak'}
      </button>

      {error && <p style={{ color: 'red' }}>{error}</p>}

      {audioUrl && (
        <audio controls autoPlay src={audioUrl} style={{ marginTop: '12px' }} />
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Finally, drop the component onto your homepage (pages/index.js or app/page.jsx):

// pages/index.js
import Head from 'next/head';
import VoicePlayer from '@/components/VoicePlayer';

export default function Home() {
  return (
    <>
      <Head>
        <title>ElevenLabs TTS Demo</title>
      </Head>
      <main style={{ padding: '2rem' }}>
        <h1>Text‑to‑Speech with ElevenLabs</h1>
        <VoicePlayer />
      </main>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Run the dev server:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000, type a sentence, click Speak, and you should hear natural‑sounding audio in seconds.


5. Adding Voice Cloning (Optional)

ElevenLabs also lets you create a custom voice from a short audio sample (as little as 30 seconds). The workflow is:

  1. Upload a WAV/MP3 file to the /v1/voices/add endpoint.
  2. Receive a voice_id you can reuse in the TTS request.

Here’s a quick curl example (replace YOUR_API_KEY and YOUR_FILE_PATH):

curl -X POST "https://api.elevenlabs.io/v1/voices/add" \
  -H "xi-api-key: YOUR_API_KEY" \
  -H "Content-Type: multipart/form-data" \
  -F "name=MyCustomVoice" \
  -F "files[]=@YOUR_FILE_PATH"
Enter fullscreen mode Exit fullscreen mode

The response includes:

{
  "voice_id": "abc123def456...",
  "name": "MyCustomVoice",
  "preview_url": "https://..."
}
Enter fullscreen mode Exit fullscreen mode

Store that voice_id in your database or local storage, then pass it to the /api/tts endpoint:

await fetch('/api/tts', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ text, voice_id: 'abc123def456...' })
});
Enter fullscreen mode Exit fullscreen mode

Now your app can speak with your brand’s voice or a character you recorded—perfect for podcasts, games, or accessibility tools.


6. Production Considerations

Concern Recommendation
Rate limits ElevenLabs free tier allows ~200 requests/min. Cache results when possible (e.g., for static blog posts).
Audio size The API returns MP3 (~16 KB per second). For longer texts, consider chunking and concatenating on the client.
Security Keep the API key server‑only. If you need client‑side access (e.g., for user‑generated voices), generate short‑lived signed tokens on the server.
SSR vs. CSR The TTS call is purely client‑side, but you could pre‑render audio URLs at build time for static content.
Error handling ElevenLabs returns detailed JSON errors on 4xx/5xx. Forward those to the client for better UX.

7. Wrap‑Up

You now have a fully functional Next.js integration that:

  • Sends text to ElevenLabs (https://try.elevenlabs.io/kr07zfuqn1bp).
  • Streams back high‑quality MP3 audio.
  • Plays the result instantly in the browser.
  • Optionally supports custom voice cloning.

Because the heavy lifting stays on the server, your front‑end stays lightweight, and you retain full control over authentication, caching, and analytics.


Ready to give your app a voice?

Head over to ElevenLabs (https://try.elevenlabs.io/kr07zfuqn1bp), grab an API key, and start experimenting. The combination of Next.js’s developer ergonomics and ElevenLabs’s cutting‑edge TTS will let you build everything from accessibility readers to AI‑driven characters in minutes. Happy coding!

Top comments (0)