API Layer
Build on the @repo/api Hono backend with typed RPC, public /v1 routes, and user API keys.
The backend is one Hono app in @repo/api, exporting a configured app plus the AppType type that powers end-to-end RPC. Everything mounts under apiBasePath (the pathname of your API URL): internal routes back the Nuxt frontend, public /v1 routes serve external integrations.
Route tree
packages/api/src/index.ts composes internal, public, and Inngest sub-routers into one routes object.
| Group | Mount | Access | Source |
|---|---|---|---|
| Health | /v1/health | Public | routes/public/v1/health.ts |
| IP | /v1/ip | Public | routes/public/v1/ip.ts |
| Content | /v1/content | Public when config.contentApi.enabled | routes/public/v1/content.ts |
| OpenAPI spec | /openapi.json, /llms.txt | Public | routes/public/index.ts |
| Auth, billing, dashboard | /auth, /billing, /dashboard | Internal | routes/internal/ |
| Admin, API keys, audit | /admin, /api-keys, /audit | Internal | routes/internal/ |
| Notifications, storage, geo | /notifications, /storage, /geo | Internal | routes/internal/ |
| Background jobs | /inngest | Inngest-signed | routes/inngest.ts |
/v1/healthreturns{ status: "success", version }with an ETag; deploy smoke tests readversion./v1/contentis filesystem-backed CRUD over markdown (Node.js runtimes only), gated byconfig.contentApi.enabledplus anX-Content-Api-Keyheader matchingCONTENT_API_KEY; when off the route is never mounted.- CORS is driven by
config.originswithcredentials: true. A global limiter caps external traffic at 200 requests / 60s; internal requests skip it viaisInternalRequest.
Typed RPC and validation
AppType captures every route, but only under two rules.
- Chain handlers: keep each router on one fluent
new Hono().get(...).post(...)chain. Reassigningappbetween calls drops routes from inference. - Validate with
sValidatorfrom@hono/standard-validatoragainst Zod schemas; read parsed input withc.req.valid("json" | "query" | "param").
// packages/api/src/index.ts
const routes = new Hono()
.route("/", internalRoutes)
.route("/", publicRoutes)
.route("/", inngestRoutes);
app.basePath(apiBasePath).route("/", routes);
export type AppType = typeof routes;Scalar API docs
When config.apiDocs is true (default), an interactive Scalar reference mounts at {apiBasePath}/docs - /api/docs, not /api/v1/docs - reading {apiBasePath}/openapi.json. Set it false to remove the route.
config.performanceMonitor.enabled (default true) toggles the Hono request logger (middleware/performance.ts, logs METHOD /path STATUS Nms cached). See Performance monitoring.
The Nuxt client
The typed client lives at apps/web-nuxt/app/utils/api.ts - hc<AppType> wired to NUXT_PUBLIC_API_URL with credentials: "include" and a custom apiFetch (api-fetch.ts). Never import the backend into app code.
- Browser: plain
globalThis.fetch; the cookie rides oncredentials. - SSR, same-origin: forwards the incoming
cookieheader and routes the call through Nuxt's$fetch.raw(path-only) - in-process, no self-request HTTP hairpin. - SSR, cross-origin (separate backend): absolute
globalThis.fetch, still forwarding the cookie.
Call api inside useAsyncData; for authenticated reads pair it with authClient.useSession(ssrFetch). See Data fetching for the full SSR patterns.
User API keys
User-facing keys (the Better Auth api-key plugin) are gated by config.apiKeys.enabled and served from routes/internal/api-keys.ts behind authGuard. Each key bakes in a per-plan rate limit at creation, resolved by resolveApiKeyRateLimitPlan (@repo/payments) from pricingConfig. Plugin config, routes, and quotas: API keys.
Frequently asked questions
Why doesn't my new route appear in the client's types?
The route chain was broken. Keep every .route()/.get() on the single fluent chain that feeds export type AppType = typeof routes - a detached call is invisible to inference.
Why are the docs at /api/docs and not under /v1?
Scalar mounts at the base path ({apiBasePath}/docs), not the versioned namespace. Toggle it with config.apiDocs.
Do SSR calls make a real HTTP request back to the server?
No. For same-origin SSR, apiFetch routes through Nuxt's $fetch.raw in-process, avoiding the self-request hairpin. Cross-origin (separate backend) does use real fetch.
Can an external client call internal routes with an API key?
A valid key authenticates any route that accepts it, but /v1/* is the documented public surface. Treat non-/v1 groups as first-party.