Project Structure
The scaffold's backend consists of three workspace packages, each doing one thing. Dependencies flow one way:
core → api-v1 → server
↗
packages/web (only with --web)pnpm -r build follows the topological order, so a package is always built before the packages that depend on it.
Generated Tree
my-app/
package.json scripts delegate to the three backend packages
pnpm-workspace.yaml backend/* and packages/*
backend/
core/ contract layer
types/ handler types, common shapes, framework aliases
src/access/ openApi / authedApi, and the guard installed by authedApi
api-v1/ one API version
src/api/ get/health.ts, post/login.ts, post/article.ts
src/ router.ts, doc.ts, mount.ts
version.js API_VERSION and API_BASE_URL
types/api.d.ts the contract's entry point (exports["./api"])
tealina.config.ts apiDir / apiBaseURL / typesDir / handlerImport
docs/api.json documentation generated by gdoc
server/ host
src/ app.ts, doc.ts, index.ts, static asset routes
public/ served by the static asset routes
packages/web/ only with --web
src/
main.ts the page
api/client.ts req derived from the api-v1 handlersbackend/core: The Contract Layer
core contains no routes. It defines what a handler is, and exposes that definition to the other packages through its own exports.
| File | Role |
|---|---|
types/handler.d.ts | OpenHandler / AuthedHandler, plus the projections that derive documentation and client types |
types/alias.d.ts | The only framework-specific file: binds the Express / Fastify / Koa request and response types to the generic handler shape |
types/common.d.ts | Shared shapes such as AuthedLocals, AuthHeaders, JsonHeaders |
src/access/index.ts | The two ways to end a file: openApi / authedApi |
src/access/verifyToken.ts | The guard authedApi puts at the head of the chain |
exports exposes three entry points: ./handler and ./common (both pure types), and ./access (runtime). So wherever a handler sits in the route tree, its import specifier is core/handler or core/access; there are no relative paths to count.
backend/api-v1: One API Version
api-v1 owns the full lifecycle of one version: the route tree, the router, the documentation source and mounting.
| File | Role |
|---|---|
src/api/ | The route tree. The first level is the HTTP method, and the file path is the URL |
src/router.ts | Hands the barrel file generated by align to @tealina/server, turning it into framework routes |
src/doc.ts | Publishes this version's docSource and serves the JSON file at <API_BASE_URL>/_doc/api.json |
src/mount.ts | Mounts the body parsers, the routes and the documentation JSON file onto the framework instance, and returns docSource |
version.js | The single place API_VERSION and API_BASE_URL are written |
types/api.d.ts | The contract entry point. ApiTypesForDoc must be the first export, with ApiTypesForClient immediately after it |
tealina.config.ts | apiDir, apiBaseURL, typesDir, handlerImport |
exports exposes three entry points:
./api: only thetypescondition, pointing attypes/api.d.ts. The frontend must useimport type../version: the runtimeAPI_VERSION/API_BASE_URL../mount:typespoints atsrc/mount.ts, andimportpoints atdist/mount.js. The host uses it to compile and mount the entire route tree.
The names of the public artifacts are fixed: types/api.d.ts, docs/api.json, exports["./api"]. Moving or renaming apiDir only changes route discovery and the handlers' own imports; it changes no public name.
backend/server: The Host
server creates the framework instance, reads .env, mounts each API version, and serves the documentation page and static assets. It owns no routes:
src/app.tscalls each version'smountand collects the returneddocSource.src/doc.tsusesassembleHTMLfrom@tealina/doc-uito generate a single documentation page listing every version the host mounts.src/index.tsstarts listening onPORT.
Error handling and 404 responses are not provided by the scaffold; they are left to framework defaults (Express's finalhandler, Koa's ctx.onerror), and the application can take over with its own strategy.
packages/web: Optional Frontend
Generated only with --web. It is a Vite project with no UI framework, demonstrating the client rather than the page:
src/api/client.tsimportsApiTypesForClientfromapi-v1/apiand createsreq.src/main.tscallsGET /healthand writes the result into the page; you can delete it and rewrite it freely.vite.config.tsproxiesAPI_BASE_URLtohttp://localhost:8000, so the backend template needs no CORS.
A Second API Version
Add backend/api-v2, make it depend on core, and have server mount it alongside api-v1. Neither core nor the frontend hard-codes a version number; each version publishes its own documentation JSON file, and the host hands every docSource to the same documentation page.