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 tsx

Add to .env:

YOUTUBE_API_KEY=your_key_here

Transcript 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

src/lib.ts
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) : null

Error handling

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

src/routes/video.ts
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 = app

Channel routes

src/routes/channel.ts
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 = app

Playlist routes

src/routes/playlist.ts
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 = app

Transcript routes

src/routes/transcript.ts
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 = app

Transcript routes import from lyra-sdk/transcript — no API key or yt() client needed.


URL routes

src/routes/url.ts
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 = app

App entry point

src/index.ts
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)

MethodPathDescription
GET/api/video/:idFetch full video details
GET/api/videos?ids=...Batch fetch videos
GET/api/video/:id/titleTitle-only lookup
GET/api/channel/:idChannel metadata
GET/api/channel/:id/videosRecent 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/routes/comment.ts
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 = app

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/routes/batch-transcript.ts
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 = app

Video Categories (API key required)

MethodPathDescription
GET/api/video-categories?regionCode=USCategories for a region
GET/api/video-category/:idSingle category by ID
src/routes/video-category.ts
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 = app

I18n (API key required)

MethodPathDescription
GET/api/regionsList supported regions
GET/api/languagesList supported languages
src/routes/i18n.ts
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 = app

Running

npx tsx src/index.ts

On this page