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`.