@flarecart/worker-http
v1.0.0
Published
The HTTP layer of a Cloudflare Worker API — a URLPattern router with per-route CORS, ApiError responses, request helpers (IP and /64, country, bearer, cookies, size-capped JSON with any Zod-shaped schema), safe cookies, origin checks and Turnstile.
Maintainers
Readme
@flarecart/worker-http
The HTTP layer of a Cloudflare Worker API: a small URLPattern router
where each route carries its own CORS policy, errors that answer
themselves, and the request helpers every Worker ends up writing (the
client's IP and country, a bearer token, a cookie, a size-capped JSON body
checked against a schema). Plus safe cookies, origin checks and Turnstile.
No framework, no dependencies. It answers the API and gets out of the way:
an unknown path resolves null, so the Worker can serve its static
assets.
npm install @flarecart/worker-httpWhy
A Worker API grows the same plumbing every time, and the details are where the bugs live:
- CORS belongs beside the route. A webhook must never answer a browser, a storefront endpoint answers your site only, and a bearer-token endpoint can answer any page (it never uses cookies). A list of rules somewhere else drifts from the routes it describes.
- Errors should be data. A handler throws
forbidden('…', 'CODE')and the caller gets{ error: { code, message } }with the right status and headers. A library's own errors (a refused API key) map to one throughonError. Everything else is a bug: logged, and answered with a 500 that leaks nothing. - IPv6 clients own a /64. Counting single addresses hands an attacker
2⁶⁴ fresh limits;
ipPrefixcounts the network. - A body is read once, and capped.
readJsonrefuses an oversized body by its declared length and then its real one (a client can lie), and names the first invalid field.
Features
- 🧭 Router on the standard
URLPattern, with params, preflights and 405 +Allow - 🌐 CORS per route:
site(your origins),any(bearer-token APIs),none(webhooks) - 🧯
ApiError: status, code, human message, headers (Retry-After), details - 🔌
onErrorto map a library's errors; bugs become a logged, generic 500 - 🛰️ Request helpers: IP, IPv6 /64, country (no Tor/unknown), user agent, bearer, cookie, query,
Accept - 📦
readJson: size-capped, validated by any Zod-shaped schema (Zod is not a dependency) - 🍪 Cookies:
HttpOnly; Secure; SameSite=Strictby default; header-splitting values refused - 🛡️ Origin checks and Turnstile verification
- 📄
htmlPage: an escaped, CSP-locked page for people who follow links
Usage
import { createRouter, route, readJson, notFound, notConfigured, originList } from '@flarecart/worker-http';
import { z } from 'zod';
const Body = z.object({ email: z.email() });
const routes = [
route('POST', '/api/checkout', 'site', async (ctx) => {
const { email } = await readJson(ctx.request, Body);
return Response.json(await ctx.startCheckout(email));
}),
route('GET', '/api/orders/:id', 'any', async (ctx, { id }) => {
const order = await ctx.findOrder(id);
if (!order) throw notFound('No such order.');
return Response.json(order);
}),
route('POST', '/api/webhooks/stripe', 'none', (ctx) => ctx.webhook()),
];
const handle = createRouter(routes, {
allowedOrigins: (ctx) => originList(ctx.env.ALLOWED_ORIGINS),
onError: (thrown) => (thrown?.type === 'StripeAuthenticationError' ? notConfigured('Stripe refused the key.') : null),
});
export default {
async fetch(request, env) {
const ctx = { request, env, /* your helpers */ };
return (await handle(request, ctx)) ?? env.ASSETS.fetch(request);
},
};API
Router
| | |
|---|---|
| route(methods, path, cors, handler) | cors is 'site', 'any' or 'none'; handler(context, params) returns a Response. |
| createRouter(routes, options?) | Returns handle(request, context) → Response, or null for an unknown path. Options: allowedOrigins (array, or a function of the context), onError(thrown) → ApiError \| null, log, cors (allowHeaders, exposeHeaders, maxAge). |
Errors
new ApiError(status, code, message, headers?, details?), and
badRequest, unauthorized, forbidden, notFound, conflict,
tooLarge, tooMany(message?, retryAfter?), notConfigured.
Responses
json(data, status?, headers?), noContent(headers?),
errorResponse(error), internalError(), htmlPage(status, title, message),
corsHeaders(origin, options?), withHeaders(response, headers),
BASE_HEADERS (nosniff, no-store, no-referrer).
Requests
getIp, ipPrefix, maskIp (203.0.113.x, for a sign-in history), getCountry, getUserAgent(request, max?),
getOrigin, getBearerToken, getCookie(request, name),
getQueryParam(request, name), wantsHtml,
readJson(request, schema, maxBytes?).
Cookies, origins, Turnstile
serializeCookie(name, value, options?), clearCookie(name, options?),
originOf(url), originList(sites), checkOrigin(request, allowed, message?),
verifyTurnstile(token, { secret, ip?, fetch? }),
requireTurnstile(request, token, { secret, fetch? }), escapeHtml.
Notes
URLPatternis built into Workers, Deno, browsers and Node 23.8+. On Node 22, load a polyfill (urlpattern-polyfill) before the router.getIpandgetCountrytrustCF-Connecting-IPandrequest.cf, which Cloudflare sets. Behind anything else, read your own trusted header.anyis only safe for endpoints that authenticate with a bearer token. An endpoint that reads a cookie must besiteornone.requireTurnstileis a no-op without a secret, so Turnstile can be switched on by setting one.
License
MIT
