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 });
}
}
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;
}
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'),
});
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>;
}
Gotchas I Hit
- Not all videos have transcripts. Some creators disable captions.
- Auto-generated captions have errors. Especially with technical terms.
- YouTube occasionally changes internal endpoints. Pin your dependency versions.
- 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)