Skip to main content

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's path in owly.yaml.
  • c.param, c.query, and c.header cover 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.