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-sdk

Add to .env.local:

YOUTUBE_API_KEY=your_key_here

Transcript 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

src/lib/youtube.ts
import { yt } from 'lyra-sdk'

const apiKey = process.env.YOUTUBE_API_KEY ?? ''

export const client = apiKey ? yt(apiKey) : null

Error helper

src/lib/errors.ts
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

src/app/api/video/[id]/route.ts
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)
  }
}
src/app/api/video/title/[id]/route.ts
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)
  }
}
src/app/api/videos/route.ts
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

src/app/api/channel/[id]/route.ts
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)
  }
}
src/app/api/channel/[id]/videos/route.ts
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

src/app/api/playlist/[id]/route.ts
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)
  }
}
src/app/api/playlist/[id]/info/route.ts
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)
  }
}
src/app/api/playlist/[id]/ids/route.ts
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)
  }
}
src/app/api/playlist/[id]/query/route.ts
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

src/app/api/transcript/[id]/route.ts
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)
  }
}
src/app/api/transcript/[id]/languages/route.ts
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)
  }
}
src/app/api/transcript/[id]/srt/route.ts
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)
  }
}
src/app/api/transcript/[id]/vtt/route.ts
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)
  }
}
src/app/api/transcript/[id]/text/route.ts
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

src/app/api/url/parse/route.ts
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)
  }
}
src/app/api/url/extract/route.ts
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)

MethodPathDescription
GET/api/video/:idFetch full video details
GET/api/video/title/:idTitle-only lookup
GET/api/videos?ids=...Batch fetch videos
GET/api/channel/:idChannel metadata
GET/api/channel/:id/videos?limit=5Recent uploads

Playlist (API key required)

MethodPathDescription
GET/api/playlist/:idFull playlist with videos
GET/api/playlist/:id/infoMetadata only
GET/api/playlist/:id/idsVideo IDs only
POST/api/playlist/:id/queryFilter/sort/range query

Transcript (no API key)

MethodPathDescription
GET/api/transcript/:idFetch transcript (?lang=en)
GET/api/transcript/:id/languagesAvailable caption tracks
GET/api/transcript/:id/srtSRT format
GET/api/transcript/:id/vttWebVTT format
GET/api/transcript/:id/textPlain text

Comments (API key required)

MethodPathDescription
GET/api/comments/:videoIdComment threads for a video
GET/api/comments/:videoId/topTop comments by relevance
GET/api/comment-replies/:idAll replies for a comment
src/app/api/comments/[videoId]/route.ts
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)
  }
}
src/app/api/comments/[videoId]/top/route.ts
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)
  }
}
src/app/api/comment-replies/[id]/route.ts
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)

MethodPathDescription
POST/api/url/parseParse YouTube URL
POST/api/url/extractExtract IDs from URL

Batch Transcript (API key required)

MethodPathDescription
POST/api/playlist/:id/transcriptBatch transcripts for a playlist
src/app/api/playlist/[id]/transcript/route.ts
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)

MethodPathDescription
GET/api/video-categories?regionCode=USCategories for a region
GET/api/video-category/[id]Single category by ID
src/app/api/video-categories/route.ts
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)
  }
}
src/app/api/video-category/[id]/route.ts
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)

MethodPathDescription
GET/api/regionsList supported regions
GET/api/languagesList supported languages
src/app/api/regions/route.ts
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)
  }
}
src/app/api/languages/route.ts
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 dev

Deploy to Vercel with zero configuration — each route.ts becomes a serverless function.

On this page