Hono File-Based API Server
A typed, file-based HTTP server built on Hono, designed to ship one (or many) Firebase Cloud Function v2 per logical API. It pairs a tiny prebuild codegen (frs gen) with a typed multi-API registry (createApiRegistry) so you can write Zod-validated routes next to your business logic and forget about wiring.
Feature overview
| Feature | Description |
|---|---|
| File-based routing | Drop a routes.ts next to a useCase. The CLI scans the tree at build time and emits a static manifest — zero runtime filesystem access. |
| Multi-API registry | createApiRegistry({ v1, v2, webhooks, ... }) is the single source of truth. Each tag becomes one Cloud Function. |
Typed defineRoute / useCaseRoute | The api field is narrowed to your registered tags. useCaseRoute(UseCaseClass, meta) derives input / output from the useCase's static Zod schemas; defineRoute({...}) stays available for inline handlers. |
| Zod validation | input schemas validated automatically (body / query / params). Optional response validation via validateOutput. |
| OpenAPI 3.1 | Auto-generated from the Zod schemas. /openapi.json + interactive Scalar UI at /docs. |
| Interceptor + onError | Single around-style hook per API for envelopes, error mapping, tracing. Plus a Hono-style onError. |
| Middlewares | Per-API and per-route Hono middlewares with full type propagation. |
| Typed context | Augment Hono's ContextVariableMap once and c.get("user") is fully typed in every handler. |
| CLI scaffolder | frs init bootstraps apis.ts + manifest stub. frs new scaffolds a useCase + route + Vitest test (interactive prompts when flags are missing). |
| One function per API | apis.toFunctions(routes, onRequest, { defaults, per }) returns a map ready to spread into your index.ts. |
Install
npm i @lpdjs/firestore-repo-service hono @hono/node-server zod
npm i -D @asteasolutions/zod-to-openapiThe frs CLI is exposed via the package's bin field.
Bootstrap a project
npx frs initThe interactive prompt asks for:
- the domain root (default
src/domains), - the
apis.tslocation (defaultsrc/apis.ts), - the list of API tags to register (default
v1), - an optional shared basePath.
Pass --yes to skip prompts (CI mode), or any of --root, --apis-file, --apis, --base-path, --force to override.
After init you'll have:
src/
├── apis.ts ← createApiRegistry(...) + export defineRoute / useCaseRoute
└── domains/
└── __generated__/routes.ts ← empty stub (refreshed by `frs gen`)Wire it in your Cloud Functions entrypoint
// src/index.ts
import { onRequest } from "firebase-functions/v2/https";
import { apis } from "./apis.js";
import { routes } from "./domains/__generated__/routes.js";
export const { v1, v2 } = apis.toFunctions(routes, onRequest, {
defaults: { region: "us-central1", invoker: "public" },
per: {
v2: { memory: "512MiB" },
},
});Each registered API tag produces one Cloud Function whose name matches the key. URLs end up at https://<region>-<project>.cloudfunctions.net/v1/....
Configure your APIs (apis.ts)
import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
import { enrichUser } from "./middlewares/enrich-user.js";
export const apis = createApiRegistry({
v1: {
basePath: "/v1",
middlewares: [enrichUser],
openapi: {
info: { title: "Public API", version: "1.0.0" },
securitySchemes: {
bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
},
security: [{ bearerAuth: [] }],
},
interceptor: async ({ next, c }) => {
try {
const data = await next();
return c.json({ success: true, data, error: null });
} catch (err) {
// map domain errors → HTTP, rethrow others to onError
throw err;
}
},
onError: (err, c) => {
console.error("Unhandled:", err);
return c.json({ error: "Internal Server Error" }, 500);
},
validateOutput: process.env["NODE_ENV"] !== "production",
verbose: process.env["NODE_ENV"] !== "production",
},
webhooks: {
basePath: "/hooks",
openapi: { info: { title: "Webhooks", version: "1.0.0" } },
},
});
// Re-export the typed helpers used in every routes.ts.
export const defineRoute = apis.defineRoute;
export const useCaseRoute = apis.useCaseRoute;Write a route
npx frs new createPost --domain posts --method post --api v1Generates:
src/domains/posts/useCases/createPost/
├── routes.ts ← maps the useCase to an HTTP endpoint
├── posts.createPost.useCase.ts ← business logic + Zod input/output schemas
└── posts.createPost.useCase.test.ts ← Vitest skeletonThe useCase / test files are prefixed with <domain>.<name> so every file is unique across the project (Ctrl+P / fuzzy-finder friendly) instead of a sea of identical useCase.ts. routes.ts keeps its name — it is the scan anchor for frs gen.
The useCase owns its Zod input / output schemas (as static members, the single source of truth) and the business logic. The shared services container is injected by the UseCase base class via the constructor:
// src/domains/posts/useCases/createPost/posts.createPost.useCase.ts
import { z } from "zod";
import { UseCase } from "@lpdjs/firestore-repo-service/servers/hono";
import type { Services } from "../../../../services.js";
const input = z.object({ title: z.string() });
const output = z.object({ id: z.string() });
export class PostsCreatePostUseCase extends UseCase<typeof input, typeof output, Services> {
static readonly input = input;
static readonly output = output;
async execute(payload: z.infer<typeof input>): Promise<z.infer<typeof output>> {
const user = this.services.ctx.c.get("user");
return { id: `${user.id}:${payload.title}` };
}
}routes.ts then wires the useCase into an endpoint with useCaseRoute — no schema duplication, no handler boilerplate:
// src/domains/posts/useCases/createPost/routes.ts
import { defineRoutes } from "@lpdjs/firestore-repo-service/servers/hono";
import { useCaseRoute } from "../../../../apis.js";
import { PostsCreatePostUseCase } from "./posts.createPost.useCase.js";
export default defineRoutes([
useCaseRoute(PostsCreatePostUseCase, {
api: "v1", // ← typed: "v1" | "webhooks"
method: "post",
summary: "Create a post",
tags: ["posts"],
}),
]);The URL is derived from the file path: posts/useCases/createPost → /posts/createPost. Combined with the v1 basePath above and the function name, the final URL is …/v1/v1/posts/createPost (or …/v1/posts/createPost if you only set basePath: "/"). You can also set path explicitly in the useCaseRoute meta.
frs new prompts interactively when flags are missing (route name, domain, method, api, with-usecase, with-test). Pass --yes to accept defaults.
Need full control? Use defineRoute
When a route has no useCase (or you want the handler inline), defineRoute takes the schemas + handler directly:
import { z } from "zod";
import { defineRoutes } from "@lpdjs/firestore-repo-service/servers/hono";
import { defineRoute } from "../../../../apis.js";
export default defineRoutes([
defineRoute({
api: "v1",
method: "post",
input: z.object({ title: z.string() }),
output: z.object({ id: z.string() }),
handler: async ({ input }) => ({ id: input.title }),
}),
]);Same endpoint, several APIs
Add more entries to the defineRoutes([...]) array — each useCaseRoute (or defineRoute) is typed independently:
export default defineRoutes([
useCaseRoute(CreatePostUseCase, { api: "v1", method: "post", tags: ["posts"] }),
useCaseRoute(CreatePostUseCase, { api: "v2", method: "post", tags: ["posts"] }),
]);Refresh the manifest
npx frs gen --root src/domainsWire it into package.json as a prebuild step:
{
"scripts": {
"build": "frs gen --root src/domains && tsc -p tsconfig.build.json",
"build:watch": "tsc -w -p tsconfig.build.json"
}
}Useful flags: --out, --routes-file, --skip, --casing kebab, --ext .js, --exclude, --silent.
Typing c.get("user") etc.
Augment Hono's variable map once (anywhere in your project):
// src/types/hono.d.ts
import "hono";
declare module "hono" {
interface ContextVariableMap {
user: { id: string; name: string; email: string };
}
}Then inside any handler / middleware, c.get("user") is fully typed — no generics to plumb through.
Services & dependency injection
Declare every singleton your project needs — repositories, SDK clients, loggers — once in a global container and let the server inject it into every handler, interceptor, cron job, trigger or test.
Why
Without DI, every route has to new MyUseCase() and forward c so the useCase can read c.get("user"). That's boilerplate-heavy and couples your business code to Hono.
With the built-in container:
- Each service is instantiated lazily on first access and cached for the process lifetime — ideal for Cloud Functions cold-start.
- Inter-service dependencies are inferred by destructuring the factory argument — no manual wiring.
- A built-in
ctxservice exposes the current request's HonoContextviaAsyncLocalStorage, so useCases can readthis.services.ctx.c.get("user")without ever receivingcas a parameter.
Declare the container (src/services.ts)
The container holds shared infrastructure only — SPIs, repositories, SDK clients, loggers. UseCases live outside the container: they extend the UseCase base class and receive the whole services container through the constructor (injected for you by useCaseRoute). That boundary keeps Services free of any reference back to itself (no circular type alias) and makes useCases trivial to unit-test with hand-rolled fakes.
import { createServices } from "@lpdjs/firestore-repo-service/servers/hono";
import { PostRepo } from "./domains/posts/PostRepo.js";
import { BigQuery } from "@google-cloud/bigquery";
export const services = createServices({
postRepo: ({ ctx }) => new PostRepo(ctx),
bigquery: () => new BigQuery({ projectId: "..." }),
});
export type Services = typeof services;Two provider forms: factory
({ ctx, db }) => new PostRepo(db)(recommended, deps explicit) or classpostRepo: PostRepo(auto-injects the full proxy). Only use the class form for SPIs that don't importServices, otherwise TypeScript will emit "type alias refers to itself circularly" and inferany.
Wire it into the registry (src/apis.ts)
import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
import { services } from "./services.js";
export const apis = createApiRegistry(
{
v1: { basePath: "/v1", openapi: { info: { title: "API", version: "1.0.0" } } },
},
{ services },
);
export const defineRoute = apis.defineRoute;
export const useCaseRoute = apis.useCaseRoute;Use services in a route
useCaseRoute wires the useCase to the endpoint and injects the shared services container automatically — the handler is generated for you:
import { defineRoutes } from "@lpdjs/firestore-repo-service/servers/hono";
import { useCaseRoute } from "../../../../apis.js";
import { CreatePostUseCase } from "./useCase.js";
export default defineRoutes([
useCaseRoute(CreatePostUseCase, { api: "v1", method: "post", tags: ["posts"] }),
]);Inside defineRoute (inline handlers) the same services proxy is available on the handler context if you ever need it directly:
defineRoute({
api: "v1",
method: "post",
input: z.object({ title: z.string() }),
output: z.object({ id: z.string() }),
handler: async ({ input, services }) =>
new CreatePostUseCase(services).execute(input),
});Read this.services inside a useCase
A useCase extends UseCase<typeof input, typeof output, Services>: the base class injects the shared services container via the constructor and the static schemas drive the typing of execute. Read deps through this.services — never store Services as a hand-written field type.
import { z } from "zod";
import { UseCase } from "@lpdjs/firestore-repo-service/servers/hono";
import type { Services } from "../../../../services.js";
const input = z.object({ title: z.string() });
const output = z.object({ id: z.string() });
export class CreatePostUseCase extends UseCase<typeof input, typeof output, Services> {
static readonly input = input;
static readonly output = output;
async execute(payload: z.infer<typeof input>): Promise<z.infer<typeof output>> {
const user = this.services.ctx.c.get("user");
return this.services.postRepo.create({ ...payload, authorId: user.id });
}
}Reuse services outside HTTP (cron, triggers, tests)
services.ctx.c throws when accessed outside a request. Wrap non-HTTP code paths in withRequestContext to supply a synthetic context, then instantiate the useCase with the shared services container:
import { withRequestContext } from "@lpdjs/firestore-repo-service/servers/hono";
import { services } from "./services.js";
import { CreatePostUseCase } from "./domains/posts/useCases/createPost/useCase.js";
export const dailyTask = onSchedule("every 24 hours", async () => {
await withRequestContext({ c: fakeContext() }, async () => {
await new CreatePostUseCase(services).execute({ title: "daily digest" });
});
});In Vitest the useCase is just a class — no withRequestContext needed, inject a hand-rolled services fake:
import { CreatePostUseCase } from "./useCase.js";
import type { Services } from "../../../../services.js";
it("creates a post", async () => {
const services = {
ctx: { c: { get: () => ({ id: "u1" }) } },
postRepo: { create: async (p: any) => ({ id: "p1", ...p }) },
} as unknown as Services;
const uc = new CreatePostUseCase(services);
expect((await uc.execute({ title: "hello" })).id).toBe("p1");
});Async resources — lazy connections
Don't make factories async — they're sync by design. Instead, lazy-load async resources inside the service:
export class BigQueryService {
private _client: BigQuery | undefined;
get client(): BigQuery {
return (this._client ??= new BigQuery({ projectId: "..." }));
}
}Scaffold a service
frs add service postRepoCreates src/services/postRepo.ts and inserts an import + a factory line into src/services.ts. Pass --services-file / --services-dir if your layout differs.
OpenAPI
When openapi.info is set on an API, the server exposes:
/<basePath>/openapi.json— the spec./<basePath>/docs— interactive Scalar UI.
The UI's data-url is computed as a relative path so it works behind Firebase emulator's prefix rewriting and reverse proxies.
Because @asteasolutions/zod-to-openapi requires Zod to be patched first, the server calls extendZodWithOpenApi(z) automatically (idempotent) — your raw Zod schemas are picked up without ceremony.
Document the interceptor envelope & errors
By default the spec documents each route's raw output. But if an interceptor wraps responses (e.g. { data, intercepted: true }), the real payload differs from output. Declare the envelope by using the structured interceptor form { output, errors?, handler } — the generator then documents what the wrapper actually returns:
// apis.ts
v1: {
openapi: { info: { title: "API", version: "1.0.0" } },
interceptor: {
// factory — `data` reflects each route's own output schema in the docs
output: (routeOutput) =>
z.object({ data: routeOutput ?? z.unknown(), intercepted: z.boolean() }),
// declared error responses, added to every operation
errors: {
400: z.object({ success: z.literal(false), error: z.string() }),
500: { description: "Internal error", schema: z.object({ error: z.string() }) },
},
handler: async ({ c, next }) => c.json({ data: await next(), intercepted: true }),
},
},outputaccepts a static schema (same envelope for every route) or a factory(routeOutput) => schema(wraps each route's ownoutput, sodatastays precisely typed per endpoint).errorskeys are HTTP status codes; values are a bare Zod schema or{ description?, schema? }. They are added to every operation.- The bare-function form (
interceptor: async ({ next }) => …) still works — it just produces no envelope metadata in the spec.
Note: the interceptor is an opaque function at runtime, so the package cannot infer its shape —
output/errorsare how you keep the docs in sync with what the wrapper returns.
Centralized error handling (errorHandler)
Extend the package's BaseErrorHandler instead of repeating a try/catch everywhere. It already maps the built-in errors (ValidationError, …); override two hooks to plug your own domain errors + logger:
mapError(ctx)→ map yourAppErrorto aResponse(returnnullto defer);logError(ctx)→ log via yourAppLogger(runs only whenmapErrormatched).
Pass an instance per API so different APIs can use different strategies.
import {
BaseErrorHandler,
type ErrorHandlerContext,
} from "@lpdjs/firestore-repo-service/servers/hono";
class AppErrorHandler extends BaseErrorHandler {
protected override mapError({ error, c }: ErrorHandlerContext): Response | null {
if (error instanceof AppError) {
const locale = c.req.header("accept-language")?.startsWith("fr") ? "fr" : "en";
return c.json(
{
// only expose the message when the error is user-facing
error: error.userFacing ? error.localizedMessage[locale] : AppError.default(locale),
errorId: error.errorId,
},
error.statusCode,
);
}
return null; // → built-in mapping via super
}
protected override logError({ error }: ErrorHandlerContext): void {
AppLogger.err(error); // structured log + correlation id
}
}
// apis.ts — per API:
v1: { ..., errorHandler: new AppErrorHandler() }, // user-facing, localized
v2: { ..., errorHandler: new BaseErrorHandler() }, // defaults only, no user-facing constraints- Auto-applied: thrown errors become proper HTTP responses with no interceptor boilerplate. If a custom interceptor rethrows, the handler still applies the
errorHandler. - Injected: available as
errorHandlerin handler/interceptor ctx for manual use (errorHandler?.handle({ error, c, route, services })). - Composable:
mapErrorreturningnulldefers to the built-in mapping; unknown errors bubble to youronError/ Hono. userFacingflag: exposelocalizedMessageonly when the error is meant for the client; otherwise return a generic message to avoid leaking internals.- A shared
errorHandlercan still be passed tocreateApiRegistry(configs, { services, errorHandler }); a per-API one overrides it.
Dev-only GCP log links (gcpLogs)
Pass { gcpLogs } to BaseErrorHandler to turn a correlation id into a one-click Cloud Logging "Logs Explorer" deep link, so a developer can jump straight from an error response to its structured log. It is opt-in — keep it off in production (the link is for engineers, not end users).
class AppErrorHandler extends BaseErrorHandler {
protected override mapError({ error, c }: ErrorHandlerContext): Response | null {
if (error instanceof AppError) {
const logsUrl = this.gcpLogsUrl(error.errorId); // undefined when disabled
return c.json(
{ error: error.message, errorId: error.errorId, ...(logsUrl ? { logsUrl } : {}) },
error.statusCode,
);
}
return null;
}
}
// apis.ts — enable outside production; projectId defaults to env GOOGLE_CLOUD_PROJECT
v1: {
...,
errorHandler: new AppErrorHandler({
gcpLogs: { enabled: process.env.NODE_ENV !== "production" },
}),
},- The link filters on
jsonPayload.errorId="<id>"— the fieldBaseLoggeralready writes — so it lands on the exact log line. - Options:
enabled(master switch),projectId(defaults toGOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT/GCP_PROJECT),field(defaulterrorId),duration(optional lookback, e.g."PT1H"). - The standalone
gcpLogsUrl(errorId, options)/resolveGcpProjectId()helpers are exported too, for use outside the error handler.
Structured logging (logger)
Symmetric to errorHandler: extend the package's BaseLogger (override the single write hook to route to your sink — Firebase logger, pino, …) and pass an instance per API. It is injected into every handler / interceptor / error-handler context as logger.
import { BaseLogger, type LogSeverity } from "@lpdjs/firestore-repo-service/servers/hono";
import { logger as fnLogger } from "firebase-functions/v2";
class AppLogger extends BaseLogger {
protected override write(severity: LogSeverity, payload: Record<string, unknown>) {
fnLogger.write({ severity, ...payload }); // one override covers info/warn/debug/error
}
}
export const appLogger = new AppLogger();
// apis.ts — per API (or shared via createApiRegistry({ services, logger })):
v1: { ..., logger: appLogger },// in a handler / interceptor / errorHandler:
handler: ({ input, logger }) => {
logger?.info("creating post", { id: input.id });
return { id: input.id };
}BaseLogger.error(err)returns a correlation id (reuseserr.errorIdwhen present, else generates one) — log it and return it to the client.- Every level (
info/warn/debug/error) funnels throughwrite, so a single override is enough. - useCases only receive
services(not the injectedlogger); expose the same instance via a project base class (protected readonly logger = appLogger) sothis.loggerand the injectedloggerare one and the same.
Protect the docs endpoints (docsAuth)
openapi.docsAuth guards only the /docs UI and /openapi.json spec — it never touches your API routes (those are protected per-API via middlewares or per-route interceptors). It accepts one Hono MiddlewareHandler or an array of them, so you can plug a fully custom flow or the built-in helpers.
Two helpers ship with the package:
import { getAuth } from "firebase-admin/auth";
import {
firebaseBearerAuth,
basicAuth,
} from "@lpdjs/firestore-repo-service/servers/hono";
// apis.ts — Firebase ID token (Bearer), with an optional allow() policy.
v1: {
openapi: {
info: { title: "API", version: "1.0.0" },
docsAuth: firebaseBearerAuth({
getAuth: () => getAuth(), // lazy — runs after initializeApp()
allow: (token) => token.admin === true, // optional, default: any verified user
}),
},
},
// …or HTTP Basic Auth:
docsAuth: basicAuth({ username: "admin", password: process.env.DOCS_PASSWORD! }),
// …or a fully custom flow (any Hono middleware):
docsAuth: async (c, next) => {
if (c.req.header("x-docs-key") !== process.env.DOCS_KEY) {
return c.text("Unauthorized", 401);
}
return next();
},firebaseBearerAuth rejects a missing/invalid token with 401 and a failed allow() with 403; the decoded token is stored on the context (c.get("docsUser") by default) for downstream use. getAuth is called lazily per request, so it is safe to declare before initializeApp() has run.
Login form + session cookie (firebaseDocsAuth)
For a browser-friendly flow — a login page + session cookie, exactly like the admin server — pass firebaseDocsAuth(...). It returns a richer value (a DocsAuthExtension) that the server expands into the bundled __login / __session / __logout routes mounted next to the docs, plus the guard. Unauthenticated browsers are redirected to the login form; once signed in, an HttpOnly session cookie keeps them in.
import { getAuth } from "firebase-admin/auth";
import { firebaseDocsAuth } from "@lpdjs/firestore-repo-service/servers/hono";
v1: {
openapi: {
info: { title: "API", version: "1.0.0" },
docsAuth: firebaseDocsAuth({
getAuth: () => getAuth(), // lazy — runs after initializeApp()
apiKey: process.env.FIREBASE_API_KEY!, // Web app config (login page SDK)
authDomain: process.env.FIREBASE_AUTH_DOMAIN!,
allow: (token) => token.admin === true, // optional policy
// mode: "both", // also accept a Bearer token (iframe)
// providers: ["password", "google"], // login page providers
}),
},
},- Modes:
"cookie"(default — login form) or"both"(also accept aBearerID token, handy to embed the docs in an authenticated iframe). - Bundled routes (
__login/__session/__logout) are mounted as siblings of the docs page, so relative links/redirects survive any Cloud Functions / reverse-proxy path prefix. - Options:
allow,providers,title,cookieName(default__docs_session),sessionTtlDays(default5),secureCookie(defaulttrue),sameSite(default"Lax"),contextKey(default"docsUser"),onUnauthenticated("redirect"|"401"). - Auth emulator: the login page's client SDK targets the Auth emulator when
authEmulatorHostis set (defaults toFIREBASE_AUTH_EMULATOR_HOST), matching the Admin SDK — so localfirebase emulators:startsign-ins work end-to-end. PassauthEmulatorHost: ""to force production.
CLI reference
| Command | Purpose |
|---|---|
frs init | Bootstrap apis.ts + an empty manifest stub. Interactive unless --yes. |
frs gen --root <dir> | Scan <dir> for routes.ts files and emit __generated__/routes.ts. |
frs new <name> --domain <d> | Scaffold a useCase + route + Vitest test. Prompts when flags are missing. |
frs add service <name> | Scaffold a service file and register it in services.ts. |
frs add server <admin|crud|sync> | Scaffold an ORM server (one file per server) + repos.ts/servers.ts. |
frs sdk:spec --entry <module> | Statically export the OpenAPI 3.1 spec to a JSON file (no server boot). |
Run frs help for the full flag list.
Static OpenAPI export (frs sdk:spec)
Export the OpenAPI 3.1 document to a file at build time — no server boot, no network — so a frontend can generate a typed SDK from it (openapi-typescript, orval, openapi-generator, …) and stay in sync with the API.
The CLI imports a Node-importable module (built JS, or run it with bunx frs sdk:spec … for TS) and reads the spec from an export that is either a CRUD server (its .spec() accessor — preserved even when wrapped via onRequest) or a plain OpenAPI document. For Hono, expose the doc via the registry's static apis.spec(api, routes):
// export-openapi.ts (Hono)
import { apis } from "./apis.js";
import { routes } from "./domains/__generated__/routes.js";
export const openapi = apis.spec("v1", routes);# Hono — from the export above
frs sdk:spec --entry lib/export-openapi.js --export openapi --out openapi.json
# CRUD — auto-detects the `.spec()` server export
frs sdk:spec --entry lib/crudServer.js --export api --out openapi.json.frsrc.json — shared config
frs init writes a .frsrc.json at the project root so sibling commands can reuse the resolved layout instead of repeating flags:
{
"root": "src/domains",
"apisFile": "src/apis.ts",
"servicesFile": "src/services.ts",
"apis": ["v1"]
}Every command reads this file and resolves each value with the precedence flag → .frsrc.json → built-in default — a flag is only applied when it is explicitly passed, otherwise the config value (if any) wins, then the default.
| Key | Type | Used by |
|---|---|---|
root | string | gen (--root becomes optional), new |
out | string | gen (output file) |
apis | string[] | new (first entry is the default --api) |
useCaseFolder | string | new |
apisFile / servicesFile / servicesDir | string | add service |
The file is optional: if it is missing or contains invalid JSON it is ignored silently. You can also edit it by hand.
Programmatic API (escape hatches)
The barrel @lpdjs/firestore-repo-service/servers/hono also exports:
HonoServer<TEnv>— the underlying server class (use directly for custom mounts or unit tests).apis.serverFor(tag, routes)— get theHonoServerfor a specific API.buildOpenApiDocument(routes, options)/renderDocsHtml(...)— generate the spec / UI HTML outside an HTTP context (e.g. in build scripts).- Codegen primitives:
scanRoutes,generateRoutesManifest,generateFromRoot,derivePath,toImportSpecifier— for users who want to bypass the CLI and integrate directly into their own pipeline. ValidationError— instance check inside your interceptor when you want to translate Zod failures into your own error envelope.
