API Server

Express API Server

REST API that wraps all lyra-sdk functions

A complete Express REST API that wraps every lyra-sdk function. Use it to test endpoints in Postman or as a starting point for your own API.

Getting Started

cd packages/sdk-examples
npm run dev:express
cd packages/sdk-examples
pnpm dev:express
cd packages/sdk-examples
bun dev:express

Runs on http://localhost:3000 (override with PORT env var).

Requires YOUTUBE_API_KEY in your .env file.

Architecture

index.ts
app.ts
lib.ts
errors.ts
video.ts
channel.ts
playlist.ts
transcript.ts
url.ts
video.ts
channel.ts
playlist.ts
transcript.ts
url.ts
FilePurpose
lib.tsInitializes the yt() client singleton
app.tsCreates the Express app, mounts routes under /api
errors.tsMaps SDK errors to HTTP responses
routes/Express routers for each resource
schemas/Zod validation schemas for each endpoint

Endpoints

See the API Reference for full interactive documentation of every endpoint.

Video

MethodPathDescription
GET/api/video/:idFetch full video details by ID or URL
GET/api/videos?ids=...Batch fetch multiple videos
GET/api/video/:id/titleLightweight title-only lookup

Channel

MethodPathDescription
GET/api/channel/:idFetch channel metadata (ID, @username, or URL)
GET/api/channel/:id/videos?limit=5Fetch recent uploads for a channel

Playlist

MethodPathDescription
GET/api/playlist/:idFull playlist with all videos
GET/api/playlist/:id/infoMetadata only (1 quota unit)
GET/api/playlist/:id/idsAll video IDs in a playlist
POST/api/playlist/:id/queryFilter, sort, and slice playlist videos

Transcript

Transcript endpoints do not require a YouTube API key. They use YouTube's Innertube API directly.

MethodPathDescription
GET/api/transcript/:idFetch transcript (?lang=en optional)
GET/api/transcript/:id/languagesList available caption tracks
GET/api/transcript/:id/srtTranscript as SRT subtitle format
GET/api/transcript/:id/vttTranscript as WebVTT format

routes/transcript.ts

routes/transcript.ts
import type { Request, Response } from 'express'
import { Router } from 'express'
import {
  transcribeVideo,
  listCaptionTracks,
  toSRT,
  toVTT,
} from 'lyra-sdk/transcript'
import { transcriptIdParam, transcriptQuery } from '../schemas/transcript.js'

const router = Router()

router.get('/transcript/:id', async (req: Request, res: Response) => {
  const { id } = transcriptIdParam.parse(req.params)
  const { lang } = transcriptQuery.parse(req.query)
  const lines = await transcribeVideo(id, { lang })
  res.json(lines)
})

router.get('/transcript/:id/languages', async (req: Request, res: Response) => {
  const { id } = transcriptIdParam.parse(req.params)
  const tracks = await listCaptionTracks(id)
  res.json(tracks)
})

router.get('/transcript/:id/srt', async (req: Request, res: Response) => {
  const { id } = transcriptIdParam.parse(req.params)
  const { lang } = transcriptQuery.parse(req.query)
  const lines = await transcribeVideo(id, { lang })
  res.type('text/plain').send(toSRT(lines))
})

router.get('/transcript/:id/vtt', async (req: Request, res: Response) => {
  const { id } = transcriptIdParam.parse(req.params)
  const { lang } = transcriptQuery.parse(req.query)
  const lines = await transcribeVideo(id, { lang })
  res.type('text/plain').send(toVTT(lines))
})

export const transcriptRoutes = router

schemas/transcript.ts

schemas/transcript.ts
import { z } from 'zod'

export const transcriptIdParam = z.object({
  id: z.string().min(1, 'Video ID or URL is required'),
})

export const transcriptQuery = z.object({
  lang: z.string().optional(),
})

Comments (API key required)

MethodPathDescription
GET/api/comments/:videoIdFetch comment threads for a video (?order=relevance&search=keyword)
GET/api/comments/:videoId/topTop comments by relevance (?limit=10)
GET/api/comment-replies/:idAll replies for a comment

routes/comment.ts

routes/comment.ts
import type { Request, Response } from 'express'
import { Router } from 'express'
import { client } from '../lib.js'

const router = Router()

router.get('/comments/:videoId', async (req: Request, res: Response) => {
  const { videoId } = req.params
  const maxResults = req.query.maxResults ? Number(req.query.maxResults) : 100
  const threads = await client!.comments(videoId, {
    order: req.query.order as any,
    searchTerms: req.query.search as string,
    maxResults,
  })
  res.json(threads)
})

router.get('/comments/:videoId/top', async (req: Request, res: Response) => {
  const { videoId } = req.params
  const limit = req.query.limit ? Number(req.query.limit) : 10
  const threads = await client!.topComments(videoId, limit)
  res.json(threads)
})

router.get('/comment-replies/:id', async (req: Request, res: Response) => {
  const replies = await client!.commentReplies(req.params.id)
  res.json(replies)
})

export const commentRoutes = router

URL Utilities

MethodPathDescription
POST/api/url/parseParse a YouTube URL into structured data
POST/api/url/extractExtract IDs from a YouTube URL

Batch Transcript

Batch transcript requires a YouTube API key to fetch the playlist's video list.

MethodPathDescription
POST/api/playlist/:id/transcriptBatch fetch transcripts for all videos in a playlist

routes/batch-transcript.ts

routes/batch-transcript.ts
import type { Request, Response } from 'express'
import { Router } from 'express'
import { transcribePlaylist } from 'lyra-sdk/transcript'
import { z } from 'zod'

const router = Router()

const batchSchema = z.object({
  concurrency: z.coerce.number().min(1).max(20).optional().default(3),
  from: z.coerce.number().min(1).optional(),
  to: z.coerce.number().min(1).optional(),
  lang: z.string().optional(),
})

router.post('/playlist/:id/transcript', async (req: Request, res: Response) => {
  const { id } = req.params
  const opts = batchSchema.parse(req.body)

  const result = await transcribePlaylist(id, {
    apiKey: process.env.YOUTUBE_API_KEY!,
    ...opts,
    onProgress(done, total, videoId, status) {
      console.log(`  [${status}] ${done}/${total}${videoId}`)
    },
  })

  res.json(result)
})

export const batchTranscriptRoutes = router

Video Categories

MethodPathDescription
GET/api/video-categories?regionCode=USFetch categories for a region
GET/api/video-category/:idFetch a single category

routes/video-category.ts

routes/video-category.ts
import type { Request, Response } from 'express'
import { Router } from 'express'
import { client } from '../lib.js'

const router = Router()

router.get('/video-categories', async (req: Request, res: Response) => {
  const regionCode = (req.query.regionCode as string) ?? 'US'
  const hl = req.query.hl as string | undefined
  const categories = await client!.videoCategoriesByRegion(regionCode, hl)
  res.json(categories)
})

router.get('/video-category/:id', async (req: Request, res: Response) => {
  const category = await client!.videoCategory(req.params.id)
  res.json(category)
})

export const videoCategoryRoutes = router

I18n

MethodPathDescription
GET/api/regionsList supported regions
GET/api/languagesList supported languages

routes/i18n.ts

routes/i18n.ts
import type { Request, Response } from 'express'
import { Router } from 'express'
import { client } from '../lib.js'

const router = Router()

router.get('/regions', async (req: Request, res: Response) => {
  const regions = await client!.regions(req.query.hl as string | undefined)
  res.json(regions)
})

router.get('/languages', async (req: Request, res: Response) => {
  const languages = await client!.languages(req.query.hl as string | undefined)
  res.json(languages)
})

export const i18nRoutes = router

Error Handling

All errors follow the same format:

{
  "error": {
    "code": 404,
    "message": "Video not found: INVALID_ID"
  }
}
HTTP StatusCause
400Validation error (Zod) or invalid URL
401Invalid YouTube API key
404Video/channel/playlist/transcript not found
429YouTube API quota exceeded or transcript rate limited
502YouTube API error
500Internal server error

On this page