API Server
Next.js API Routes
REST API using Next.js App Router route.ts files
Next.js App Router uses route.ts files to create API endpoints. This example shows how to use lyra-sdk in Next.js server-side route handlers.
Setup
npx create-next-app@latest lyra-next-api --typescript --app
cd lyra-next-api
npm install lyra-sdkAdd to .env.local:
YOUTUBE_API_KEY=your_key_hereTranscript routes do not require YOUTUBE_API_KEY.
File structure
route.ts
route.ts
route.ts
route.ts
route.ts
route.ts
route.ts
route.ts
route.ts
route.ts
route.ts
route.ts
route.ts
route.ts
route.ts
route.ts
youtube.ts
errors.ts
Shared utilities
Client initialization
import { yt } from 'lyra-sdk'
const apiKey = process.env.YOUTUBE_API_KEY ?? ''
export const client = apiKey ? yt(apiKey) : nullError helper
import { NextResponse } from 'next/server'
import { AuthError, NotFoundError, QuotaError, YTError } from 'lyra-sdk'
import { TranscriptError } from 'lyra-sdk/transcript'
export function handleError(err: unknown) {
if (err instanceof NotFoundError) {
return NextResponse.json({ error: { code: 404, message: err.message } }, { status: 404 })
}
if (err instanceof AuthError) {
return NextResponse.json({ error: { code: 401, message: err.message } }, { status: 401 })
}
if (err instanceof QuotaError) {
return NextResponse.json({ error: { code: 429, message: err.message } }, { status: 429 })
}
if (err instanceof TranscriptError) {
return NextResponse.json({ error: { code: 404, message: err.message } }, { status: 404 })
}
if (err instanceof YTError) {
return NextResponse.json({ error: { code: 502, message: err.message } }, { status: 502 })
}
return NextResponse.json(
{ error: { code: 500, message: 'Internal server error' } },
{ status: 500 },
)
}Video routes
import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const video = await client!.video(id)
return NextResponse.json(video)
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const title = await client!.videoTitle(id)
return NextResponse.json(title)
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const ids = (searchParams.get('ids') ?? '').split(',').map((s) => s.trim())
const videos = await client!.videos(ids)
return NextResponse.json(videos)
} catch (err) {
return handleError(err)
}
}Channel routes
import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const channel = await client!.channel(id)
return NextResponse.json(channel)
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const { searchParams } = new URL(request.url)
const limit = Number(searchParams.get('limit')) || undefined
const videos = await client!.channelVideos(id, { limit })
return NextResponse.json(videos)
} catch (err) {
return handleError(err)
}
}Playlist routes
import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const playlist = await client!.playlist(id)
return NextResponse.json(playlist)
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const info = await client!.playlistInfo(id)
return NextResponse.json(info)
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const ids = await client!.playlistVideoIds(id)
return NextResponse.json({ id, videoIds: ids, count: ids.length })
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const body = await request.json()
let query = client!.playlistQuery(id)
if (body.filter?.duration) query = query.filterByDuration(body.filter.duration)
if (body.filter?.views) query = query.filterByViews(body.filter.views)
if (body.filter?.likes) query = query.filterByLikes(body.filter.likes)
if (body.sort) query = query.sortBy(body.sort.field, body.sort.order)
if (body.range) query = query.between(body.range.start, body.range.end)
const result = await query.execute()
return NextResponse.json(result)
} catch (err) {
return handleError(err)
}
}Transcript routes
import { NextResponse } from 'next/server'
import { transcribeVideo } from 'lyra-sdk/transcript'
import { handleError } from '@/lib/errors'
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const { searchParams } = new URL(request.url)
const lang = searchParams.get('lang') ?? undefined
const lines = await transcribeVideo(id, { lang })
return NextResponse.json(lines)
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { listCaptionTracks } from 'lyra-sdk/transcript'
import { handleError } from '@/lib/errors'
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const tracks = await listCaptionTracks(id)
return NextResponse.json(tracks)
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { transcribeVideo, toSRT } from 'lyra-sdk/transcript'
import { handleError } from '@/lib/errors'
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const { searchParams } = new URL(request.url)
const lang = searchParams.get('lang') ?? undefined
const lines = await transcribeVideo(id, { lang })
return new NextResponse(toSRT(lines), {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
})
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { transcribeVideo, toVTT } from 'lyra-sdk/transcript'
import { handleError } from '@/lib/errors'
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const { searchParams } = new URL(request.url)
const lang = searchParams.get('lang') ?? undefined
const lines = await transcribeVideo(id, { lang })
return new NextResponse(toVTT(lines), {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
})
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { transcribeVideo, toPlainText } from 'lyra-sdk/transcript'
import { handleError } from '@/lib/errors'
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const { searchParams } = new URL(request.url)
const lang = searchParams.get('lang') ?? undefined
const lines = await transcribeVideo(id, { lang })
return new NextResponse(toPlainText(lines), {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
})
} catch (err) {
return handleError(err)
}
}Transcript route files import from lyra-sdk/transcript — no API key or client singleton needed.
URL routes
import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function POST(request: Request) {
try {
const { url } = await request.json()
const result = client!.url.parse(url)
return NextResponse.json(result)
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function POST(request: Request) {
try {
const { url, type } = await request.json()
if (type === 'video' || (!type && client!.url.isVideo(url))) {
return NextResponse.json({ type: 'video', videoId: client!.url.extractVideoId(url) })
}
if (type === 'playlist' || (!type && client!.url.isPlaylist(url))) {
return NextResponse.json({ type: 'playlist', playlistId: client!.url.extractPlaylistId(url) })
}
const channelId = client!.url.extractChannelId(url)
if (channelId) return NextResponse.json({ type: 'channel', channelId })
return NextResponse.json({ type: 'unknown', id: null })
} catch (err) {
return handleError(err)
}
}Endpoints
Video & Channel (API key required)
| Method | Path | Description |
|---|---|---|
| GET | /api/video/:id | Fetch full video details |
| GET | /api/video/title/:id | Title-only lookup |
| GET | /api/videos?ids=... | Batch fetch videos |
| GET | /api/channel/:id | Channel metadata |
| GET | /api/channel/:id/videos?limit=5 | Recent uploads |
Playlist (API key required)
| Method | Path | Description |
|---|---|---|
| GET | /api/playlist/:id | Full playlist with videos |
| GET | /api/playlist/:id/info | Metadata only |
| GET | /api/playlist/:id/ids | Video IDs only |
| POST | /api/playlist/:id/query | Filter/sort/range query |
Transcript (no API key)
| Method | Path | Description |
|---|---|---|
| GET | /api/transcript/:id | Fetch transcript (?lang=en) |
| GET | /api/transcript/:id/languages | Available caption tracks |
| GET | /api/transcript/:id/srt | SRT format |
| GET | /api/transcript/:id/vtt | WebVTT format |
| GET | /api/transcript/:id/text | Plain text |
Comments (API key required)
| Method | Path | Description |
|---|---|---|
| GET | /api/comments/:videoId | Comment threads for a video |
| GET | /api/comments/:videoId/top | Top comments by relevance |
| GET | /api/comment-replies/:id | All replies for a comment |
import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(
request: Request,
{ params }: { params: { videoId: string } },
) {
try {
const { searchParams } = new URL(request.url)
const maxResults = searchParams.get('maxResults') ? Number(searchParams.get('maxResults')) : 100
const threads = await client!.comments(params.videoId, {
order: searchParams.get('order') as any,
searchTerms: searchParams.get('search') ?? undefined,
maxResults,
})
return NextResponse.json(threads)
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(
request: Request,
{ params }: { params: { videoId: string } },
) {
try {
const { searchParams } = new URL(request.url)
const limit = searchParams.get('limit') ? Number(searchParams.get('limit')) : 10
const threads = await client!.topComments(params.videoId, limit)
return NextResponse.json(threads)
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(
request: Request,
{ params }: { params: { id: string } },
) {
try {
const replies = await client!.commentReplies(params.id)
return NextResponse.json(replies)
} catch (err) {
return handleError(err)
}
}URL Utilities (API key required)
| Method | Path | Description |
|---|---|---|
| POST | /api/url/parse | Parse YouTube URL |
| POST | /api/url/extract | Extract IDs from URL |
Batch Transcript (API key required)
| Method | Path | Description |
|---|---|---|
| POST | /api/playlist/:id/transcript | Batch transcripts for a playlist |
import { NextResponse } from 'next/server'
import { transcribePlaylist } from 'lyra-sdk/transcript'
import { handleError } from '@/lib/errors'
export async function POST(
request: Request,
{ params }: { params: { id: string } },
) {
try {
const body = await request.json().catch(() => ({}))
const result = await transcribePlaylist(params.id, {
apiKey: process.env.YOUTUBE_API_KEY!,
concurrency: body.concurrency ?? 3,
from: body.from,
to: body.to,
lang: body.lang,
})
return NextResponse.json(result)
} catch (err) {
return handleError(err)
}
}Video Categories (API key required)
| Method | Path | Description |
|---|---|---|
| GET | /api/video-categories?regionCode=US | Categories for a region |
| GET | /api/video-category/[id] | Single category by ID |
import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const regionCode = searchParams.get('regionCode') ?? 'US'
const hl = searchParams.get('hl') ?? undefined
const categories = await client!.videoCategoriesByRegion(regionCode, hl)
return NextResponse.json(categories)
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(
request: Request,
{ params }: { params: { id: string } },
) {
try {
const category = await client!.videoCategory(params.id)
return NextResponse.json(category)
} catch (err) {
return handleError(err)
}
}I18n (API key required)
| Method | Path | Description |
|---|---|---|
| GET | /api/regions | List supported regions |
| GET | /api/languages | List supported languages |
import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const regions = await client!.regions(searchParams.get('hl') ?? undefined)
return NextResponse.json(regions)
} catch (err) {
return handleError(err)
}
}import { NextResponse } from 'next/server'
import { client } from '@/lib/youtube'
import { handleError } from '@/lib/errors'
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const languages = await client!.languages(searchParams.get('hl') ?? undefined)
return NextResponse.json(languages)
} catch (err) {
return handleError(err)
}
}Running
npm run devDeploy to Vercel with zero configuration — each route.ts becomes a serverless function.