AGENTS.md@templates/api · git:20261021.01ecd81 · 2026-10-21 · sha256 4e718d79012c3683
AGENTS.md@templates/api git:20261021.01ecd81B
Immutable. This exact content never changes and is served at /api/v1/blob/4e718d79012c3683.
# bun-api-template-app
HTTP API on Bun + Elysia, with SQLite (`bun:sqlite`), JWT auth (`jose`), and OpenAPI docs.
Generated by create-bun-api (`api` template; see `create-bun-api.json`).
## Commands
| Task | Command |
| --- | --- |
| Install | `bun install` (CI: `bun install --frozen-lockfile`) |
| Run with reload | `bun run dev` → http://localhost:3000, docs at `/docs` |
| Test | `bun test` · one folder: `bun test src/modules/users` |
| Typecheck · lint · autofix | `bun run typecheck` · `bun run lint` · `bun run format` |
| **Prove it works** | **`bun run verify`** (`--json` for machine-readable output) |
`bun run verify` must pass before you report a task as done. It needs no network and no `.env`.
## Layout
```
src/index.ts process entry: config → database → listen. Nothing else.
src/app.ts createApp(): builds services and mounts modules. No listen (tests use app.handle).
src/config.ts every env var, validated. New vars go here AND in .env.example.
src/db/ SQLite client and append-only migrations (migrations.ts).
src/lib/ framework-free helpers: errors, logger, password, tokens.
src/plugins/ cross-cutting Elysia plugins: errors, request-log, auth.
src/modules/<name>/ one folder per feature:
<name>.schema.ts <name>.repository.ts <name>.service.ts <name>.routes.ts <name>.test.ts
test/helpers.ts createTestApp(), request(), signUp().
scripts/verify.ts typecheck → lint → test → boot → GET /health.
```
## Conventions
- **New feature:** add a `src/modules/<name>/` folder modeled on `src/modules/users/` and mount it in `src/app.ts`.
- **Routes** declare `body`, `query`, `params`, and `response` with `t` schemas. Invalid input then
becomes a 400 automatically, and the route appears in `/docs`.
- **Errors:** throw the helpers from `src/lib/errors.ts` (`badRequest`, `unauthorized`, `forbidden`,
`notFound`, `conflict`). Never return `{ status, body }` objects: Elysia sends them with HTTP 200.
- **Success with a status other than 200:** `({ status }) => status(201, value)`.
- **Protected routes:** `.use(authPlugin(deps.tokens))` in the module, set `{ auth: true }` on the route,
and read `session.userId`.
- **SQL** lives only in `*.repository.ts`, as prepared statements with `$named` parameters
(bind them without the `$`). Repositories return domain objects, never password hashes.
- **Schema changes:** append a migration to `src/db/migrations.ts`. Never edit one that has shipped.
- **Logging:** `log.info("event", { fields })`. Never log tokens, passwords, headers, or request bodies.
Redaction is a safety net, not permission.
- **Tests:** use `createTestApp()` (a fresh in-memory database per call). Assert the HTTP status and the
error `code`, not just the body.
- **Prefer Bun built-ins:** `bun:sqlite`, `Bun.password`, `bun test`, `Bun.serve`. Don't add dotenv,
express, jest, or bcrypt.
## Gotchas
- Bun loads `.env` automatically. `bun test` does too, so tests never read config from `process.env`.
- In tests, keep `http://localhost` in request URLs. Elysia misreads very short hosts like `http://x/`
and answers 404.
- `onAfterResponse` hooks run after `app.handle()` resolves. `request()` in `test/helpers.ts` waits for them.
- Password hashing is deliberately slow (argon2id, 64 MiB). Don't lower it to speed up tests.
## Skills
Step-by-step guides live in `.agents/skills/`, with the same content in `.claude/skills/`:
`add-endpoint`, `add-model`, `verify-and-debug`.