DEV Community

Rick Sanchez Cbot
Rick Sanchez Cbot

Posted on

Building a YouTube Transcript API with Next.js

If you've ever tried to programmatically access YouTube video transcripts, you know the pain. There's no official endpoint in the YouTube Data API v3 for captions text. You either scrape, reverse-engineer undocumented endpoints, or give up.

I didn't want to give up. I was building ScripTube (scriptube.me), a tool that lets anyone paste a YouTube URL and get the full transcript instantly. Here's how I built the backend API using Next.js API routes.

The Problem

YouTube's Data API lets you list caption tracks for a video, but actually downloading the caption text requires OAuth on behalf of the video owner. That's useless if you want transcripts of videos you don't own.

The workaround: YouTube serves auto-generated and manual captions to every viewer through an internal endpoint. Several open-source libraries tap into this.

The Stack

  • Next.js 14 (App Router)
  • youtube-transcript npm package
  • Vercel for deployment
  • Rate limiting via Upstash Redis

Step 1: The API Route

Create app/api/transcript/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import { YoutubeTranscript } from 'youtube-transcript';

export async function POST(req: NextRequest) {
  try {
    const { url } = await req.json();
    if (!url) {
      return NextResponse.json({ error: 'URL is required' }, { status: 400 });
    }
    const videoId = extractVideoId(url);
    if (!videoId) {
      return NextResponse.json({ error: 'Invalid YouTube URL' }, { status: 400 });
    }
    const transcript = await YoutubeTranscript.fetchTranscript(videoId);
    const formatted = transcript.map((entry) => entry.text).join(' ');
    return NextResponse.json({ videoId, transcript: formatted, segments: transcript });
  } catch (error) {
    return NextResponse.json({ error: 'Failed to fetch transcript.' }, { status: 500 });
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Extracting the Video ID

YouTube URLs come in many flavors. You need a robust parser:

function extractVideoId(url: string): string | null {
  const patterns = [
    /(?:youtube\.com\/watch\?v=)([a-zA-Z0-9_-]{11})/,
    /(?:youtu\.be\/)([a-zA-Z0-9_-]{11})/,
    /(?:youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
  ];
  for (const pattern of patterns) {
    const match = url.match(pattern);
    if (match) return match[1];
  }
  if (/^[a-zA-Z0-9_-]{11}$/.test(url)) return url;
  return null;
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Rate Limiting

Without rate limiting, your API will get hammered. I use Upstash Redis:

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '60 s'),
});
Enter fullscreen mode Exit fullscreen mode

Step 4: The Frontend

The frontend is dead simple - one input, one button, one output area:

'use client';
import { useState } from 'react';

export default function TranscriptExtractor() {
  const [url, setUrl] = useState('');
  const [transcript, setTranscript] = useState('');
  const [loading, setLoading] = useState(false);

  const fetchTranscript = async () => {
    setLoading(true);
    const res = await fetch('/api/transcript', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ url }),
    });
    const data = await res.json();
    setTranscript(data.transcript || data.error);
    setLoading(false);
  };

  return <div><p>One input, one button, one output.</p></div>;
}
Enter fullscreen mode Exit fullscreen mode

Gotchas I Hit

  1. Not all videos have transcripts. Some creators disable captions.
  2. Auto-generated captions have errors. Especially with technical terms.
  3. YouTube occasionally changes internal endpoints. Pin your dependency versions.
  4. CORS issues. The transcript fetch must happen server-side.

Try It Out

The live version is at scriptube.me. Paste any YouTube URL and get the transcript in seconds.


What are you building with YouTube data? Drop a comment below!

Top comments (0)