Skip to main content

Custom routes

Temba's built-in routes always return JSON. If your API needs routes that return something else (an image, a PDF, a CSV export), you can combine Temba with your own Node.js HTTP server, all on a single port.

Before reaching for this

Check whether an existing Temba feature already covers your need:

Custom routes are the right tool when you need a response format that isn't JSON: binary data, CSV, plain text, etc.

How it works

create() returns a server property: the raw Node.js http.Server that Temba uses internally. You can delegate requests to it from your own server using Node's standard emit('request', req, res).

Your server runs in front. It handles the routes it knows about and falls through to Temba for everything else:

import { create } from 'temba'
import { createServer } from 'node:http'
import { readFile } from 'node:fs/promises'

const temba = await create()
// deliberately NOT calling temba.start()

const server = createServer(async (req, res) => {
// Serve a PNG image from disk
if (req.method === 'GET' && req.url === '/logo.png') {
const image = await readFile('./public/logo.png')
res.writeHead(200, { 'Content-Type': 'image/png' })
res.end(image)
return
}

// Everything else goes to Temba
temba.server.emit('request', req, res)
})

server.listen(8362)

The key detail: do not call temba.start(). Temba will not listen on any port unless you explicitly call start() or server.listen(). Your outer server owns the port.

Why this works

temba.server is a standard Node.js http.Server. Its internal routing lives in a request event listener. Calling emit('request', req, res) fires that listener directly with the original request and response objects, with no proxy, no extra overhead, and no second port.

URL structure

RequestHandled by
GET /logo.pngYour custom handler
GET /report.csvYour custom handler
GET /moviesTemba
POST /moviesTemba
GET /openapi.jsonTemba

Custom routes take priority because your handler runs first. If your handler writes a response and returns, Temba never sees the request.

Overriding a Temba route

Your custom handler also runs before Temba for any route, including ones Temba would normally handle. This means you can override Temba's response for a specific route:

const server = createServer(async (req, res) => {
// Return a CSV instead of Temba's JSON for GET /movies
if (req.method === 'GET' && req.url === '/movies') {
const movies = await fetchMoviesFromSomewhere()
const csv = movies.map((m) => `${m.id},${m.title}`).join('\n')
res.writeHead(200, { 'Content-Type': 'text/csv' })
res.end(`id,title\n${csv}`)
return
}

temba.server.emit('request', req, res)
})

Using an API prefix

If you configure an apiPrefix, your Temba routes are scoped under that prefix. Your custom routes are entirely separate and unaffected:

const temba = await create({ apiPrefix: 'api' })

const server = createServer(async (req, res) => {
if (req.method === 'GET' && req.url === '/logo.png') {
// custom route, no prefix needed
res.writeHead(200, { 'Content-Type': 'image/png' })
res.end(await readFile('./public/logo.png'))
return
}

temba.server.emit('request', req, res)
})

server.listen(8362)
RequestHandled by
GET /logo.pngYour custom handler
GET /api/moviesTemba
POST /api/moviesTemba

For isolated, runnable examples of the features used here, see example 11 (API Prefix) in the Examples section.