A TypeScript function, end to end
A worked example: a small leaderboard API that shows path params, query params,
request headers, and JSON responses. It builds on the
@owly/runtime reference.
Scaffold
owly init --lang ts arcade
cd arcade && npm install
owly add --lang ts leaderboard # mounted at /api/leaderboard
The handler
src/leaderboard/index.ts:
import { createApp } from '@owly/runtime';
type Scope = 'global' | 'regional' | 'local';
const TOTAL: Record<Scope, number> = { global: 10_000, regional: 1_000, local: 100 };
const app = createApp();
const names = ['ByteWrangler', 'NullPointer', 'StackOverflow', 'RaceCondition', 'OffByOne'];
function isScope(s: string): s is Scope {
return s === 'global' || s === 'regional' || s === 'local';
}
app.get('/api/leaderboard/:scope', (c) => {
// Path param.
const raw = c.param('scope') ?? 'global';
const scope: Scope = isScope(raw) ? raw : 'global';
// Query params, with clamping + defaults.
const page = Math.max(1, Number(c.query('page') ?? '1') || 1);
const limit = Math.min(20, Math.max(1, Number(c.query('limit') ?? '10') || 10));
// Request header.
const playerScore = Number(c.header('x-player-score') ?? '0') || 0;
const offset = (page - 1) * limit;
const entries = Array.from({ length: limit }, (_, i) => ({
rank: offset + i + 1,
name: names[(offset + i) % names.length],
score: Math.max(0, 5_000_000 - (offset + i) * 1234),
}));
return c.json(
{ scope, page, limit, total: TOTAL[scope], entries },
{
headers: {
'X-Total-Players': String(TOTAL[scope]),
'Cache-Control': 'no-store',
...(playerScore > 0 ? { 'X-Your-Rank': String(estimateRank(scope, playerScore)) } : {}),
},
},
);
});
function estimateRank(scope: Scope, score: number): number {
const frac = Math.max(0, 1 - score / 5_000_000);
return Math.max(1, Math.ceil(TOTAL[scope] * frac));
}
app.listen();
Handling a POST with a JSON body
app.post('/api/leaderboard/:scope/submit', async (c) => {
const body = await c.json<{ name: string; score: number }>();
if (!body.name || typeof body.score !== 'number') {
return c.json({ error: 'name and score required' }, { status: 400 });
}
return c.json({ accepted: true, scope: c.param('scope'), name: body.name }, { status: 201 });
});
Run it
owly build
owly deploy
Then:
curl 'https://arcade.<host>/api/leaderboard/global?page=2&limit=5' \
-H 'X-Player-Score: 250000'
{ "scope": "global", "page": 2, "limit": 5, "total": 10000, "entries": [ … ] }
What to take from this
- Routes see the full path (
/api/leaderboard/:scope), matching the function'spathinowly.yaml. c.param,c.query, andc.headercover the common input sources; response builders (c.json,c.text,c.html) set sensible defaults you can override.- Handlers are request-scoped; derive constants at module scope (like
names) rather than relying on cross-request state. See Cold starts.