API Server
Hono API Server
REST API with Hono — works on Bun, Deno, Node, and edge runtimes
Hono is a lightweight web framework that runs on any JavaScript runtime. This example wraps every lyra-sdk function as REST endpoints using Hono.
Setup
mkdir lyra-hono-api && cd lyra-hono-api
npm init -y
npm install hono lyra-sdk zod dotenv
npm install -D @hono/node-server tsxAdd to .env:
YOUTUBE_API_KEY=your_key_hereTranscript endpoints do not require YOUTUBE_API_KEY.
File structure
index.ts
lib.ts
errors.ts
video.ts
channel.ts
playlist.ts
transcript.ts
url.ts
Initialization
import { config } from 'dotenv'
config()
import { yt } from 'lyra-sdk'
const API_KEY = process.env.YOUTUBE_API_KEY ?? ''
export const client = API_KEY ? yt(API_KEY) : nullError handling
import type { ErrorHandler } from 'hono'
import { AuthError, NotFoundError, QuotaError, YTError } from 'lyra-sdk'
import { TranscriptError } from 'lyra-sdk/transcript'
export const onError: ErrorHandler = (err, c) => {
if (err instanceof NotFoundError) {
return c.json({ error: { code: 404, message: err.message } }, 404)
}
if (err instanceof AuthError) {
return c.json({ error: { code: 401, message: err.message } }, 401)
}
if (err instanceof QuotaError) {
return c.json({ error: { code: 429, message: err.message } }, 429)
}
if (err instanceof TranscriptError) {
const status = 'status' in err ? (err as any).status ?? 500 : 500
return c.json({ error: { code: status, message: err.message } }, status)
}
if (err instanceof YTError) {
return c.json({ error: { code: 502, message: err.message } }, 502)
}
console.error('Unhandled error:', err)
return c.json({ error: { code: 500, message: 'Internal server error' } }, 500)
}Video routes
import { Hono } from 'hono'
import { client } from '../lib.js'
const app = new Hono()
app.get('/video/:id', async (c) => {
const id = c.req.param('id')
const video = await client!.video(id)
return c.json(video)
})
app.get('/videos', async (c) => {
const ids = c.req.query('ids')?.split(',').map((s) => s.trim()) ?? []
const videos = await client!.videos(ids)
return c.json(videos)
})
app.get('/video/:id/title', async (c) => {
const id = c.req.param('id')
const result = await client!.videoTitle(id)
return c.json(result)
})
export const videoRoutes = appChannel routes
import { Hono } from 'hono'
import { client } from '../lib.js'
const app = new Hono()
app.get('/channel/:id', async (c) => {
const id = c.req.param('id')
const channel = await client!.channel(id)
return c.json(channel)
})
app.get('/channel/:id/videos', async (c) => {
const id = c.req.param('id')
const limit = Number(c.req.query('limit')) || 5
const videos = await client!.channelVideos(id, { limit })
return c.json(videos)
})
export const channelRoutes = appPlaylist routes
import { Hono } from 'hono'
import { client } from '../lib.js'
const app = new Hono()
app.get('/playlist/:id', async (c) => {
const id = c.req.param('id')
const playlist = await client!.playlist(id)
return c.json(playlist)
})
app.get('/playlist/:id/info', async (c) => {
const id = c.req.param('id')
const info = await client!.playlistInfo(id)
return c.json(info)
})
app.get('/playlist/:id/ids', async (c) => {
const id = c.req.param('id')
const ids = await client!.playlistVideoIds(id)
return c.json({ id, videoIds: ids, count: ids.length })
})
app.post('/playlist/:id/query', async (c) => {
const id = c.req.param('id')
const body = await c.req.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 c.json(result)
})
export const playlistRoutes = appTranscript routes
import { Hono } from 'hono'
import {
transcribeVideo,
listCaptionTracks,
toSRT,
toVTT,
toPlainText,
} from 'lyra-sdk/transcript'
const app = new Hono()
app.get('/transcript/:id', async (c) => {
const id = c.req.param('id')
const lang = c.req.query('lang')
const lines = await transcribeVideo(id, { lang })
return c.json(lines)
})
app.get('/transcript/:id/languages', async (c) => {
const id = c.req.param('id')
const tracks = await listCaptionTracks(id)
return c.json(tracks)
})
app.get('/transcript/:id/srt', async (c) => {
const id = c.req.param('id')
const lang = c.req.query('lang')
const lines = await transcribeVideo(id, { lang })
return c.text(toSRT(lines))
})
app.get('/transcript/:id/vtt', async (c) => {
const id = c.req.param('id')
const lang = c.req.query('lang')
const lines = await transcribeVideo(id, { lang })
return c.text(toVTT(lines))
})
app.get('/transcript/:id/text', async (c) => {
const id = c.req.param('id')
const lang = c.req.query('lang')
const lines = await transcribeVideo(id, { lang })
return c.text(toPlainText(lines))
})
export const transcriptRoutes = appTranscript routes import from lyra-sdk/transcript — no API key or yt() client needed.
URL routes
import { Hono } from 'hono'
import { client } from '../lib.js'
const app = new Hono()
app.post('/url/parse', async (c) => {
const { url } = await c.req.json()
const result = client!.url.parse(url)
return c.json(result)
})
app.post('/url/extract', async (c) => {
const { url, type } = await c.req.json()
if (type === 'video' || (!type && client!.url.isVideo(url))) {
return c.json({ type: 'video', videoId: client!.url.extractVideoId(url) })
}
if (type === 'playlist' || (!type && client!.url.isPlaylist(url))) {
return c.json({ type: 'playlist', playlistId: client!.url.extractPlaylistId(url) })
}
const channelId = client!.url.extractChannelId(url)
if (channelId) return c.json({ type: 'channel', channelId })
return c.json({ type: 'unknown', id: null })
})
export const urlRoutes = appApp entry point
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { videoRoutes } from './routes/video.js'
import { channelRoutes } from './routes/channel.js'
import { playlistRoutes } from './routes/playlist.js'
import { transcriptRoutes } from './routes/transcript.js'
import { urlRoutes } from './routes/url.js'
import { onError } from './errors.js'
const app = new Hono()
app.route('/api', videoRoutes)
app.route('/api', channelRoutes)
app.route('/api', playlistRoutes)
app.route('/api', transcriptRoutes)
app.route('/api', urlRoutes)
app.onError(onError)
const PORT = Number(process.env.PORT) || 3000
serve({ fetch: app.fetch, port: PORT }, () => {
console.log(`Hono API running at http://localhost:${PORT}`)
})Endpoints
Video & Channel (API key required)
| Method | Path | Description |
|---|---|---|
| GET | /api/video/:id | Fetch full video details |
| GET | /api/videos?ids=... | Batch fetch videos |
| GET | /api/video/:id/title | Title-only lookup |
| GET | /api/channel/:id | Channel metadata |
| GET | /api/channel/:id/videos | 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 { Hono } from 'hono'
import { client } from '../lib.js'
const app = new Hono()
app.get('/comments/:videoId', async (c) => {
const maxResults = c.req.query('maxResults') ? Number(c.req.query('maxResults')) : 100
const threads = await client!.comments(c.req.param('videoId'), {
order: c.req.query('order') as any,
searchTerms: c.req.query('search'),
maxResults,
})
return c.json(threads)
})
app.get('/comments/:videoId/top', async (c) => {
const limit = c.req.query('limit') ? Number(c.req.query('limit')) : 10
const threads = await client!.topComments(c.req.param('videoId'), limit)
return c.json(threads)
})
app.get('/comment-replies/:id', async (c) => {
const replies = await client!.commentReplies(c.req.param('id'))
return c.json(replies)
})
export const commentRoutes = appURL 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 { Hono } from 'hono'
import { transcribePlaylist } from 'lyra-sdk/transcript'
const app = new Hono()
app.post('/playlist/:id/transcript', async (c) => {
const { id } = c.req.param()
const body = await c.req.json().catch(() => ({}))
const result = await transcribePlaylist(id, {
apiKey: process.env.YOUTUBE_API_KEY!,
concurrency: body.concurrency ?? 3,
from: body.from,
to: body.to,
lang: body.lang,
})
return c.json(result)
})
export const batchTranscriptRoutes = appVideo 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 { Hono } from 'hono'
import { client } from '../lib.js'
const app = new Hono()
app.get('/video-categories', async (c) => {
const regionCode = c.req.query('regionCode') ?? 'US'
const hl = c.req.query('hl')
const categories = await client!.videoCategoriesByRegion(regionCode, hl)
return c.json(categories)
})
app.get('/video-category/:id', async (c) => {
const category = await client!.videoCategory(c.req.param('id'))
return c.json(category)
})
export const videoCategoryRoutes = appI18n (API key required)
| Method | Path | Description |
|---|---|---|
| GET | /api/regions | List supported regions |
| GET | /api/languages | List supported languages |
import { Hono } from 'hono'
import { client } from '../lib.js'
const app = new Hono()
app.get('/regions', async (c) => {
const regions = await client!.regions(c.req.query('hl'))
return c.json(regions)
})
app.get('/languages', async (c) => {
const languages = await client!.languages(c.req.query('hl'))
return c.json(languages)
})
export const i18nRoutes = appRunning
npx tsx src/index.ts