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:expresscd packages/sdk-examples
pnpm dev:expresscd packages/sdk-examples
bun dev:expressRuns 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
| File | Purpose |
|---|---|
lib.ts | Initializes the yt() client singleton |
app.ts | Creates the Express app, mounts routes under /api |
errors.ts | Maps 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
| Method | Path | Description |
|---|---|---|
| GET | /api/video/:id | Fetch full video details by ID or URL |
| GET | /api/videos?ids=... | Batch fetch multiple videos |
| GET | /api/video/:id/title | Lightweight title-only lookup |
Channel
| Method | Path | Description |
|---|---|---|
| GET | /api/channel/:id | Fetch channel metadata (ID, @username, or URL) |
| GET | /api/channel/:id/videos?limit=5 | Fetch recent uploads for a channel |
Playlist
| Method | Path | Description |
|---|---|---|
| GET | /api/playlist/:id | Full playlist with all videos |
| GET | /api/playlist/:id/info | Metadata only (1 quota unit) |
| GET | /api/playlist/:id/ids | All video IDs in a playlist |
| POST | /api/playlist/:id/query | Filter, sort, and slice playlist videos |
Transcript
Transcript endpoints do not require a YouTube API key. They use YouTube's Innertube API directly.
| Method | Path | Description |
|---|---|---|
| GET | /api/transcript/:id | Fetch transcript (?lang=en optional) |
| GET | /api/transcript/:id/languages | List available caption tracks |
| GET | /api/transcript/:id/srt | Transcript as SRT subtitle format |
| GET | /api/transcript/:id/vtt | Transcript as WebVTT format |
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 = routerschemas/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)
| Method | Path | Description |
|---|---|---|
| GET | /api/comments/:videoId | Fetch comment threads for a video (?order=relevance&search=keyword) |
| GET | /api/comments/:videoId/top | Top comments by relevance (?limit=10) |
| GET | /api/comment-replies/:id | All replies for a comment |
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 = routerURL Utilities
| Method | Path | Description |
|---|---|---|
| POST | /api/url/parse | Parse a YouTube URL into structured data |
| POST | /api/url/extract | Extract IDs from a YouTube URL |
Batch Transcript
Batch transcript requires a YouTube API key to fetch the playlist's video list.
| Method | Path | Description |
|---|---|---|
| POST | /api/playlist/:id/transcript | Batch fetch transcripts for all videos in a playlist |
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 = routerVideo Categories
| Method | Path | Description |
|---|---|---|
| GET | /api/video-categories?regionCode=US | Fetch categories for a region |
| GET | /api/video-category/:id | Fetch a single category |
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 = routerI18n
| Method | Path | Description |
|---|---|---|
| GET | /api/regions | List supported regions |
| GET | /api/languages | List supported languages |
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 = routerError Handling
All errors follow the same format:
{
"error": {
"code": 404,
"message": "Video not found: INVALID_ID"
}
}| HTTP Status | Cause |
|---|---|
| 400 | Validation error (Zod) or invalid URL |
| 401 | Invalid YouTube API key |
| 404 | Video/channel/playlist/transcript not found |
| 429 | YouTube API quota exceeded or transcript rate limited |
| 502 | YouTube API error |
| 500 | Internal server error |