diff --git a/.env.example b/.env.example
index d098d10..fb48752 100644
--- a/.env.example
+++ b/.env.example
@@ -1,8 +1,21 @@
-# Description: Example of .env file
+# Example .env file for local development and tests
-# MongoDB URI
+# MongoDB (optional)
MONGO_URI=mongodb://localhost:27017/your-database-name
-# Auth Plugin
+# OpenID Connect (used by the auth plugin)
+# AUTH_DISCOVERY_URL should point to the provider's discovery document (/.well-known/openid-configuration)
AUTH_DISCOVERY_URL=https://login.microsoftonline.com/c917f3e2-9322-4926-9bb3-daca730413ca/v2.0/.well-known/openid-configuration
AUTH_CLIENT_ID=b4bc4b9a-7162-44c5-bb50-fe935dce1f5a
+
+# When true, the auth plugin skips remote discovery and verification (useful for local testing)
+AUTH_SKIP=true
+
+# Optional runtime controls
+NODE_ENV=development
+PORT=3000
+
+# Test helpers (optional)
+# Provide these only when running integration tests that require a real token/user.
+TEST_AUTH_TOKEN=
+TEST_AUTH_USER=
diff --git a/.github/workflows/docs-publish.yml b/.github/workflows/docs-publish.yml
new file mode 100644
index 0000000..bdc15b1
--- /dev/null
+++ b/.github/workflows/docs-publish.yml
@@ -0,0 +1,67 @@
+name: docs:publish
+
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+jobs:
+ detect-quota:
+ # Probe the hosted runner so we can decide whether to use hosted or self-hosted.
+ runs-on: ubuntu-slim
+ steps:
+ - name: Quota probe
+ id: quota_probe
+ run: |
+ echo "probe"
+
+ build-and-publish:
+ needs: detect-quota
+ # NOTE: do not set `continue-on-error` on detect-quota (see docs)
+ runs-on: ${{ needs.detect-quota.result == 'success' && 'ubuntu-slim' || 'self-hosted' }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24
+
+ - name: Enable Corepack
+ run: |
+ corepack enable
+ corepack install
+
+ - name: Enable Node cache
+ uses: actions/setup-node@v4
+ with:
+ cache: yarn
+
+ - name: Install dependencies (Corepack + Yarn)
+ run: |
+ corepack prepare yarn@stable --activate
+ corepack yarn install --frozen-lockfile
+
+ - name: Build project
+ run: corepack yarn build
+
+ - name: Generate TypeDoc
+ run: corepack yarn docs:typedoc
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: api-docs
+ path: docs/api
+
+ - name: Publish to GitHub Pages
+ uses: peaceiris/actions-gh-pages@v4
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./docs/api
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..23cd5f5
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,62 @@
+name: docs
+
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ detect-quota:
+ # Probe the hosted runner so we can decide whether to use a hosted
+ # runner or fall back to self-hosted. The job should fail if quota is exhausted.
+ runs-on: ubuntu-slim
+ steps:
+ - name: Quota probe
+ id: quota_probe
+ run: |
+ echo "probe"
+
+ build:
+ needs: detect-quota
+ # NOTE: Do not set `continue-on-error: true` on the `detect-quota` job.
+ # If `continue-on-error` is enabled the job result will always be
+ # 'success', which defeats detection (we rely on `needs.detect-quota.result`).
+ runs-on: ${{ needs.detect-quota.result == 'success' && 'ubuntu-slim' || 'self-hosted' }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24
+
+ - name: Enable Corepack
+ run: |
+ corepack enable
+ corepack install
+
+ - name: Enable Node cache
+ uses: actions/setup-node@v4
+ with:
+ cache: yarn
+
+ - name: Install dependencies
+ run: |
+ yarn install --frozen-lockfile
+
+ - name: Build project
+ run: |
+ yarn build
+
+ - name: Generate TypeDoc
+ run: |
+ yarn docs:typedoc
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: api-docs
+ path: docs/api
diff --git a/.gitignore b/.gitignore
index 67f48cf..3f1c7de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -76,3 +76,7 @@ dist
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+
+# Generated TypeDoc output (do not commit generated API docs)
+docs/api/
+docs/api/**
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 17820df..8ed6f78 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,16 +1,15 @@
# How to Contribute
-Thanks for reading this, because we really need everyone to follow some collaboration guidelines to make our codebase
-maintainable, clean and concise.
+Thanks for taking a moment to read this. Following these collaboration guidelines helps keep the codebase maintainable,
+clean, and easy to review.
The standard workflow to contribute to the project is as follows:
1. Create an issue to track the things (feature, bug or something else) you are working on. This also allows other
contributors to know what you are working on, and to potentially offer suggestions or help.
-2. On the right hand side of the issue page, there is a button that allows you to create a new branch for the issue.
- This will automatically create a new branch with the issue number and title. I strongly recommend you to use this
- feature instead of creating a branch manually, because it will make it easier to track the issue that you are working
- on.
+2. On the right hand side of an issue page there is a button to create a branch for that issue. It will create a branch
+ named from the issue number and title. We recommend using that feature (rather than creating branches manually) to
+ make it easier to track work against the originating issue.
3. Make your changes in the new branch. Ideally, your commit messages should follow
the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. We will rebase your PR
later if your commits are well-organized and the messages are well-formatted. Otherwise, we may ask you to fix them,
@@ -34,8 +33,8 @@ but we will try our best to achieve the following goals:
- Use iterative methods on collections instead of loops whenever possible.
- Avoid side effects in a function as much as possible.
-That's not only because functional programming is cool, but also because it reduces the complexity of the code, and
-makes it easier to reason about the code. For example, you should _never_ do:
+Apart from functional programming being cool, this reduces complexity and
+makes code easier to reason about. For example, you should _avoid_:
```typescript
const numbers = [1, 2, 3]
@@ -64,16 +63,12 @@ During the PR review process, we make use of GitHub comments to track suggestion
The general principles are the following:
- If the comment is a change suggestion...
-
- - If it's clear and uncontroversial how to apply the suggestion, you should resolve the comment after you have made
+ - If it's clear and uncontroversial how to apply the suggestion, you should resolve the comment after you have made
the corresponding changes to the PR.
-
- - If you are not 100% sure that you have applied the suggestion correctly, leave a comment asking it. Do not resolve
+ - If you are not 100% sure that you have applied the suggestion correctly, leave a comment asking it. Do not resolve
the comment in this case.
-
- - If you don't fully understand or agree with the suggestion, reply to the comment with your questions and
+ - If you don't fully understand or agree with the suggestion, reply to the comment with your questions and
rebuttals. Do not resolve the comment in this case.
-
- If the comment is a clarification request, answer it. Do not resolve the comment in this case. We will either come
back with further questions or suggestions, or close the comment ourselves if we find your answer satisfactory.
@@ -86,8 +81,5 @@ Please make sure your code passes all automated CI tests (unless under special c
You may trigger them by simply pushing your commits to a branch, or by opening a PR. Reviewing a PR that doesn't pass
tests is a waste of time for everyone involved.
-During code review, it is not supposed to happen that we have to keep coming back to your PR, continually finding more
-problems more unpolished changes, and having to go through lots of back-and-forth interactions this way. This will only
-lead to growing frustration on both ends.
-
-
+During code review, please try to address issues thoroughly before requesting another round of review. This reduces
+unnecessary back-and-forth and speeds up the process for everyone.
diff --git a/README.md b/README.md
index 5338bad..09625ef 100644
--- a/README.md
+++ b/README.md
@@ -1,52 +1,69 @@
-# USThing Template API
+# template-api
-The template repository for USThing backend services, powered by Fastify.
+
+[](https://github.com/USThing/template-api/actions/workflows/check.yml) [](https://github.com/USThing/template-api/actions/workflows/docs-publish.yml) [](https://github.com/USThing/template-api/actions/workflows/release.yml) [](./docs/api/index.html)
-## Available Scripts
+A concise Fastify + TypeScript starter used by USThing backend services. This repository provides a minimal, well-tested scaffold with recommended scripts, linting, and CI configuration.
-In the project directory, you can run:
+## Prerequisites
-### `yarn run dev`
+- Node.js (see `engines` in `package.json`)
+- Yarn via Corepack
-To start the app in dev mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+Enable Corepack (recommended) and the Yarn version used by this repo:
-### `yarn run start`
+```bash
+corepack enable
+corepack prepare yarn@stable --activate
+```
-For production mode
+## Quickstart (local)
-### `yarn run test`
+```bash
+corepack enable
+yarn install
+yarn build
+yarn start
+```
-Run the test cases.
+## Developer workflow
-### `yarn run lint`
+- Start dev mode (watch + Fastify): `yarn dev`
+- Run tests: `yarn test`
+- Lint: `yarn lint` (fix: `yarn lint:fix`)
-Run the linter.
+## Automatic API docs
-Note that the format of the code will also be checked.
+API docs are generated from source by TypeDoc and published by CI. To generate locally:
-### `yarn run lint:fix`
+```bash
+yarn docs:typedoc
+```
-Run the linter and fix the issues.
+Generated docs are placed under `docs/api` (CI publishes these artifacts — do not commit generated files).
-Note that the format of the code will also be checked and fixed.
+## Project layout
-## Environment Variables
+- `src/` — application code (routes, plugins, utils)
+- `src/app.ts` — Fastify app and plugin registration
+- `routes/` — route modules
+- `test/` — tests and helpers
+- `docs/` — human-authored guides and docs
+- `.env.example` — example environment variables
-For Fastify-level environment variables, please refer to the [fastify-cli documentation](https://github.com/fastify/fastify-cli).
+## Environment
-For the application-level environment variables, please refer to the `.env.example` file.
+Tests and some dev helpers reference `TEST_AUTH_TOKEN` / `TEST_AUTH_USER`. See `docs/env-vars.md` for recommended env variables and CI secret usage. Keep secrets out of the repo and use your CI's secret manager.
-## CI / CD
+## Contributing
-This template supports GitHub Actions for CI / CD. The available workflows are:
+Follow `CONTRIBUTING.md` (commitlint, lint, tests). The project uses Conventional Commits for releases.
-- Checks / eslint: Run ES Lint to check problems and the format of the code.
-- Checks / commitlint: Run Commitlint to check the format of the commit messages.
-- Checks / tests: Run unit tests of the project.
-- Docker CI / docker: Build the Docker image and push it to the GitHub Container Registry.
-- Release Please / release-please: Automatic releasing. See also [release-please](https://github.com/googleapis/release-please).
+## Support
-## Learn More
+Open an issue on GitHub using the provided templates for bugs or feature requests.
-To learn Fastify, check out the [Fastify documentation](https://fastify.dev/docs/latest/).
+## Learn more
+
+- Fastify:
+- Docs folder: `docs/` (detailed guides and examples)
diff --git a/docs/ci.md b/docs/ci.md
new file mode 100644
index 0000000..841b327
--- /dev/null
+++ b/docs/ci.md
@@ -0,0 +1,16 @@
+# CI workflows
+
+The repository uses three primary GitHub Actions workflows in `.github/workflows/`:
+
+- `check.yml` — runs on push and pull_request. It probes runner availability (a small `detect-quota` job) and then runs ESLint, commitlint, and tests. Each job uses a hosted-first → self-hosted fallback by selecting `runs-on` based on the probe result.
+- `docker.yml` — builds and pushes container images (Buildx). It also probes runner availability and uses a hosted-first fallback. The workflow prepares Docker metadata (tags include sha, branch/ref, and PR tags) and pushes images to `REGISTRY`/`IMAGE_NAME`.
+- `release.yml` — runs `googleapis/release-please-action@v4` on pushes to `main`. When a release is created the workflow tags versions and (optionally) builds/pushes images. See `release.yml` for the exact tagging and build steps.
+
+## Key notes
+
+- Hosted-first fallback: jobs use a small probe job (`detect-quota`) and set `runs-on` dynamically so CI prefers `ubuntu-latest` but can fall back to `self-hosted` when needed.
+- Docker workflow permissions: the docker workflow requests permissions to push packages and to request an `id-token` for registry login; it logs into the registry using the workflow token by default.
+- Release workflow: `release-please` creates release PRs or releases and exposes outputs such as `release_created`, `major`, `minor`, `patch`, `tag_name`, and `body` that downstream steps use.
+- Tokens: the action uses the default `GITHUB_TOKEN` unless configured to use a PAT; if you need CI checks to run on Release PRs, configure a PAT as described in the action docs.
+
+If a run fails, open the workflow run in GitHub Actions and inspect the primary (hosted) job logs first. Avoid committing generated artifacts; let CI produce and publish them.
diff --git a/docs/dev-setup.md b/docs/dev-setup.md
new file mode 100644
index 0000000..cb84428
--- /dev/null
+++ b/docs/dev-setup.md
@@ -0,0 +1,70 @@
+# Developer setup
+
+This file documents the minimal developer setup for the template-api project.
+
+## Prerequisites
+
+- Node.js >= 24 (LTS or newer recommended)
+- Corepack (bundled with modern Node.js releases)
+
+
+
+## Enable Corepack
+
+Enable corepack if not already enabled:
+
+```bash
+# enables Corepack globally
+corepack enable
+```
+
+## Verify setup
+
+Verify Yarn is available and the expected version
+
+```bash
+# Yarn should be available after enabling Corepack
+yarn -v
+```
+
+## Installation
+
+Install dependencies and prepare hooks
+
+```bash
+# install dependencies
+yarn install
+
+# prepare Husky git hooks (if present)
+yarn run prepare
+```
+
+## Notes
+
+- After enabling Corepack you can use `yarn` directly — there is no need to prefix commands with `corepack`.
+- If the repository sets a Yarn version via `.yarnrc.yml`/`yarn.lock`, Corepack will select the correct Yarn release automatically.
+- Keep your Node toolchain up-to-date for security and compatibility.
+
+## Troubleshooting
+
+- If `yarn` is not found after enabling Corepack, restart your shell or open a new terminal.
+- If you need a specific Yarn release, run `corepack prepare yarn@ --activate`.
+
+## Further reading
+
+- Corepack:
+- Yarn (Berry) docs:
+
+## Optional: Node version management
+
+If you choose to use a Node version manager, pin a version in the repo (for example add a `.nvmrc` with `24`) so contributors and CI pick the same Node. Minimal examples:
+
+```bash
+# nvm
+nvm install 24 && nvm use 24
+
+# fnm
+fnm install 24 && fnm use 24
+```
+
+This is optional but recommended for reproducible local builds and CI.
diff --git a/docs/docker-and-deployment.md b/docs/docker-and-deployment.md
new file mode 100644
index 0000000..8579214
--- /dev/null
+++ b/docs/docker-and-deployment.md
@@ -0,0 +1,56 @@
+# Docker & Deployment
+
+This guide explains how to build and run the project's Docker image and where CI builds/publishes images.
+
+## Provided Dockerfile
+
+A `Dockerfile` exists at the repository root. CI image build/publish logic has been extracted into an internal shared-step workflow (`.github/workflows/docker-shared-steps.yml`) and is invoked by a small top-level caller (`.github/workflows/docker.yml`) that prefers hosted runners and falls back to `self-hosted` when the hosted job fails.
+
+## Local build & run
+
+Build the image locally (from repo root):
+
+```bash
+# build
+docker build -t template-api:local .
+
+# run (expose port 3000)
+docker run --rm -p 3000:3000 --env-file .env.example template-api:local
+```
+
+Replace `--env-file .env.example` with your `.env` file or explicit `-e` flags for required environment variables.
+
+## CI image publishing
+
+The repository's `docker.yml` top-level caller invokes the internal shared-step workflow to build and push images to the registry. The shared-step workflow:
+
+- sets up QEMU and Buildx
+- logs into the registry using the workflow token
+- generates metadata tags (sha, branch/ref, test)
+- builds and pushes the image(s)
+
+If you need to change the target registry or image name, update the `REGISTRY` and `IMAGE_NAME` environment variables in the shared-step workflow or the top-level caller as appropriate.
+
+Note: the top-level caller uses `ubuntu-latest` for the primary try job; when that job fails the fallback runs on `self-hosted`. This is intended to improve reliability while keeping hosted runner usage first.
+
+## Deployment notes
+
+- Use environment variables (do not bake secrets into images).
+- Prefer a runtime secret manager (Kubernetes Secrets, cloud secret manager) rather than `.env` in production.
+- For Kubernetes, build image tags in CI (sha or semver) and deploy via your CD system pointing to those tags.
+
+## Debugging
+
+- To inspect a container locally:
+
+```bash
+docker logs
+docker exec -it /bin/sh
+```
+
+- If the app fails to start in the container, check the `NODE_ENV` and required env variables, and run the same start command from within the container to reproduce.
+
+## Further reading
+
+- Docker Docs:
+- GitHub Actions docker/build-push-action:
diff --git a/docs/env-vars.md b/docs/env-vars.md
new file mode 100644
index 0000000..fee88a1
--- /dev/null
+++ b/docs/env-vars.md
@@ -0,0 +1,30 @@
+# Environment variables
+
+This project reads configuration from environment variables. A plain-text example file is provided at `.env.example` (copy it to `.env` for local development).
+
+Recommended variables:
+
+- MONGO_URI: MongoDB connection string (optional).
+- AUTH_DISCOVERY_URL: OpenID Connect discovery URL (used by the auth plugin).
+- AUTH_CLIENT_ID: OpenID Connect client id.
+- AUTH_SKIP: When `true`, the auth plugin will skip external verification (useful for local tests).
+- NODE_ENV: `development` / `production`.
+- PORT: Port the Fastify server listens on (default 3000).
+
+Test helpers (optional):
+
+- TEST_AUTH_TOKEN: Bearer token used by integration tests (set only in CI or local secure env).
+- TEST_AUTH_USER: Expected username for the test token.
+
+How to use
+
+Copy the example to a working `.env` file and modify values as needed:
+
+```bash
+cp .env.example .env
+```
+
+Notes
+
+- Never commit real secrets. Use your cloud provider's secret manager or CI secrets for production deployments.
+- Git: the repository ignores any file starting with `.env` (for example `.env`, `.env.local`, `.env.development`) — only the `.env.example` file is tracked. This keeps local or sensitive environment files out of version control while providing an example to copy from.
diff --git a/docs/plugins.md b/docs/plugins.md
new file mode 100644
index 0000000..b8326da
--- /dev/null
+++ b/docs/plugins.md
@@ -0,0 +1,41 @@
+# Plugins
+
+This file summarizes the plugins provided in the template and how they are loaded.
+
+## Loading
+
+- Plugins live under `src/plugins/` and are autoloaded by `@fastify/autoload` in `src/app.ts`.
+- Plugins run before routes are registered so they can provide decorators, hooks, and shared schemas used by routes.
+
+## About plugins
+
+Plugins define cross-cutting behavior for your application (auth, DB, caching, helpers). Files in `src/plugins/` typically use `fastify-plugin` so they can expose decorators and hooks to the outer scope.
+
+See also:
+
+- The hitchhiker's guide to plugins:
+- Fastify decorators:
+- Fastify lifecycle:
+
+## Key plugins
+
+- `sensible.ts`
+ - Registers `@fastify/sensible` and exposes a shared schema id `HttpError` used in route schemas.
+ - Provides convenience reply helpers such as `reply.badRequest()`.
+
+- `auth.ts`
+ - Adds authentication support via OpenID Connect discovery and JWT verification.
+ - Exposes a decorator `fastify.authPlugin(request, reply)` to validate requests; also sets `request.user` after successful auth.
+ - Options: `authDiscoveryURL`, `authClientID`, and optional `authSkip` to disable in local/testing.
+
+- `init-mongo.ts` (optional)
+ - Placeholder plugin for MongoDB initialization (indexes, migrations). The actual MongoDB registration is commented out in `src/app.ts` and is optional.
+
+- `support.ts`
+ - Example plugin demonstrating `fastify.decorate()` (adds `someSupport()` decorator returning a string).
+
+## Extending plugins
+
+- Add a new plugin under `src/plugins/` and export it as a Fastify plugin (use `fastify-plugin` to expose decorators).
+- Keep plugin responsibilities narrow and reusable (auth, DB setup, metrics, tracing, etc.).
+- Document plugin options and side effects inline in the plugin and in `docs/plugins.md` when needed.
diff --git a/docs/quickstart.md b/docs/quickstart.md
new file mode 100644
index 0000000..30b1d33
--- /dev/null
+++ b/docs/quickstart.md
@@ -0,0 +1,70 @@
+# Quickstart
+
+This page expands the brief Quickstart in the project README. Keep this short and focused — detailed guides belong elsewhere in `docs/`.
+
+## Prerequisites
+
+- Node.js >= 24
+- Yarn 3+ (the repository uses Yarn; `npm` may also work but commands below use `yarn`)
+
+Corepack / Node setup: see `docs/dev-setup.md` for enabling Corepack and node version manager tips.
+
+## Install
+
+```bash
+# Install dependencies (after enabling Corepack, use `yarn`)
+yarn install
+
+# Prepare Husky git hooks (if present)
+yarn run prepare
+```
+
+## Development
+
+```bash
+# start in development (watch + fastify)
+yarn run dev
+```
+
+The server listens on the port configured in your environment (default Fastify port is typically 3000). Check logs for the bound address.
+
+## Build & production run
+
+```bash
+# compile the TypeScript output to dist
+yarn run build
+
+# start the production server (reads compiled files from dist)
+yarn run start
+```
+
+## Environment
+
+Application-level variables are documented in `.env.example` at the repository root. Don't commit secrets — use environment management for CI/production.
+
+## Example requests
+
+This template includes an example route. When running locally, try:
+
+```bash
+# Basic example (expected response: "this is an example")
+curl -sS http://localhost:3000/example/
+
+# Example that returns a bad request error (demonstrates error handling)
+curl -sS http://localhost:3000/example/error
+```
+
+Notes:
+
+- The example routes are implemented under `src/routes/example/`. The folder name is used as the route prefix by autoload (so the endpoints above are `/example/` and `/example/error`).
+- If your project config mounts routes differently, adjust the path accordingly.
+
+## API docs
+
+When Swagger/UI is enabled (see `src/plugins`), an interactive API UI is typically available at `/documentation` (or the path configured in the plugin). Generated specs should be published by CI rather than committed frequently to avoid merge conflicts.
+
+## Next steps
+
+- Read `docs/routes.md` (TODO) for a high-level route map and plugin responsibilities.
+- Add environment values to `.env` based on `.env.example`.
+- Open `CONTRIBUTING.md` before making the first PR.
diff --git a/docs/release.md b/docs/release.md
new file mode 100644
index 0000000..08b00b8
--- /dev/null
+++ b/docs/release.md
@@ -0,0 +1,12 @@
+# Release automation (summary)
+
+Releases are produced by `googleapis/release-please-action`. The release workflow has been refactored so release steps are defined in `.github/workflows/release-shared-steps.yml` and invoked by a small top-level caller (`.github/workflows/release.yml`) which prefers hosted runners and falls back to `self-hosted` when the top-level try job fails.
+
+- Trigger: push to `main`.
+- Behavior: `googleapis/release-please-action` parses Conventional Commits and will open/update Release PRs or create a release when appropriate; it also generates/updates the changelog (`CHANGELOG.md`) and exposes outputs such as `release_created`, `major`, `minor`, `patch`, `tag_name`, and `body`.
+- CI integration: the shared-step workflow consumes those outputs to tag versions and to build/push container images (see the repository workflows for exact steps). The hosted-first / self-hosted fallback may cause duplicate PR checks to appear when the fallback runs — this is cosmetic.
+
+## Notes for maintainers
+
+- Review the draft release or Release PR produced by the action, add concise highlights or migration notes, then publish/merge.
+- Do not commit a handwritten `CHANGELOG.md` to the repository; rely on per-release notes generated by the action.
diff --git a/docs/routes.md b/docs/routes.md
new file mode 100644
index 0000000..157292e
--- /dev/null
+++ b/docs/routes.md
@@ -0,0 +1,40 @@
+# Routes (overview)
+
+This file gives a short, human-oriented map of the routes and plugins in this template. Keep it brief — full API specs are generated from code (use CI to publish generated docs).
+
+## Route loading
+
+- The project uses `@fastify/autoload` in `src/app.ts` to load all files under `src/routes/`.
+- Each folder under `src/routes//` is mounted at the prefix `/` (e.g. `src/routes/example/` => `/example`).
+
+## About routes
+
+Routes define the endpoints of your application. Prefer organizing related endpoints in the same folder (e.g. `src/routes/users/`) and keep each route file a Fastify plugin (autoload will mount it automatically).
+
+If a single route file grows large, split it into a folder with an `index.ts` plugin and helper modules. Use `plugins/` for shared functionality exposed via decorators.
+
+See Fastify's route patterns and promise-resolution behavior for async handlers:
+
+## Example routes
+
+`src/routes/example/index.ts` exposes two endpoints:
+
+- GET /example/
+ - Response: 200, plain string
+ - Example: `this is an example`
+- GET /example/error
+ - Response: 400 (Bad Request) with JSON error body produced via `fastify-sensible`
+ - Example: returns a bad request with message `this is an error example`
+
+Use these endpoints for quick smoke tests while developing.
+
+## How to extend
+
+- Add route modules under `src/routes//` — autoload will mount them automatically.
+- Add plugins to `src/plugins/` for shared functionality (auth, DB init, logging, etc.).
+- When adding config-driven plugins, ensure options are declared in `AppOptions` in `src/app.ts` or passed via `--options` to Fastify start.
+
+## Where to find generated API docs
+
+- The project registers `@fastify/swagger` and `@fastify/swagger-ui` in `src/app.ts`. The interactive UI is typically served at `/documentation` unless configured otherwise.
+- Prefer generating and publishing the OpenAPI/TypeDoc output in CI rather than committing generated artifacts to the repository.
diff --git a/docs/testing-and-linting.md b/docs/testing-and-linting.md
new file mode 100644
index 0000000..4001c92
--- /dev/null
+++ b/docs/testing-and-linting.md
@@ -0,0 +1,53 @@
+# Testing & Linting
+
+This document covers how to run tests and linting locally for this project.
+
+## Run tests locally
+
+From the repository root:
+
+```bash
+# install dependencies
+yarn install
+
+# compile TypeScript
+yarn run build
+
+# run tests
+yarn run test
+```
+
+Notes
+
+- The `test` script compiles tests with `tsc -p test/tsconfig.json` and runs the compiled tests from `dist/test/` using Node's test runner.
+
+## Test structure
+
+- `test/` contains unit and integration tests and a `helper.ts` for shared utilities.
+- Keep fixtures under `test/fixtures/` and helpers under `test/helpers/`.
+- Prefer small, deterministic tests. Use environment flags (e.g. `AUTH_SKIP`) to avoid external dependencies in unit tests.
+
+## Linting and pre-commit
+
+Run lint locally:
+
+```bash
+yarn run lint
+```
+
+To fix common lint issues:
+
+```bash
+yarn run lint:fix
+```
+
+Husky may be configured to run checks on commit; run `yarn run prepare` to install hooks locally.
+
+## Troubleshooting
+
+- If TypeScript compilation for tests fails, run `tsc -p test/tsconfig.json` to inspect errors.
+- If tests are slow or flaky, isolate and run them individually and add timeouts or mocks where appropriate.
+
+## Further reading
+
+- Node.js test runner:
diff --git a/package.json b/package.json
index 1d436c2..23eb2db 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,9 @@
"lint": "eslint",
"lint:fix": "eslint --fix",
"commitlint": "commitlint --last --strict --verbose",
- "prepare": "husky"
+ "prepare": "husky",
+ "docs:typedoc": "typedoc --options typedoc.json",
+ "docs:clean": "node ./scripts/clean-docs.js"
},
"engines": {
"node": ">= 24"
@@ -60,6 +62,7 @@
"husky": "^9.1.7",
"prettier": "^3.7.4",
"prettier-plugin-jsdoc": "^1.7.0",
+ "typedoc": "^0.28.16",
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.1"
},
diff --git a/scripts/clean-docs.js b/scripts/clean-docs.js
new file mode 100644
index 0000000..f2b64a3
--- /dev/null
+++ b/scripts/clean-docs.js
@@ -0,0 +1,16 @@
+import { rm } from "fs/promises";
+
+async function main() {
+ try {
+ await rm(new URL("../docs/api", import.meta.url), {
+ recursive: true,
+ force: true,
+ });
+ console.log("docs/api removed");
+ } catch (err) {
+ console.error("failed to remove docs/api:", err);
+ process.exit(1);
+ }
+}
+
+main();
diff --git a/src/plugins/README.md b/src/plugins/README.md
deleted file mode 100644
index 1e61ee5..0000000
--- a/src/plugins/README.md
+++ /dev/null
@@ -1,16 +0,0 @@
-# Plugins Folder
-
-Plugins define behavior that is common to all the routes in your
-application. Authentication, caching, templates, and all the other cross
-cutting concerns should be handled by plugins placed in this folder.
-
-Files in this folder are typically defined through the
-[`fastify-plugin`](https://github.com/fastify/fastify-plugin) module,
-making them non-encapsulated. They can define decorators and set hooks
-that will then be used in the rest of your application.
-
-Check out:
-
-* [The hitchhiker's guide to plugins](https://fastify.dev/docs/latest/Guides/Plugins-Guide/)
-* [Fastify decorators](https://fastify.dev/docs/latest/Reference/Decorators/).
-* [Fastify lifecycle](https://fastify.dev/docs/latest/Reference/Lifecycle/).
diff --git a/src/routes/README.md b/src/routes/README.md
deleted file mode 100644
index 75b5658..0000000
--- a/src/routes/README.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Routes Folder
-
-Routes define the pathways within your application.
-Fastify's structure supports the modular monolith approach, where your
-application is organized into distinct, self-contained modules.
-This facilitates easier scaling and future transition to a microservice architecture.
-In the future you might want to independently deploy some of those.
-
-In this folder you should define all the routes that define the endpoints
-of your web application.
-Each service is a [Fastify
-plugin](https://fastify.dev/docs/latest/Reference/Plugins/), it is
-encapsulated (it can have its own independent plugins) and it is
-typically stored in a file; be careful to group your routes logically,
-e.g. all `/users` routes in a `users.js` file. We have added
-a `root.js` file for you with a '/' root added.
-
-If a single file becomes too large, create a folder and add a `index.js` file there:
-this file must be a Fastify plugin, and it will be loaded automatically
-by the application. You can now add as many files as you want inside that folder.
-In this way you can create complex routes within a single monolith,
-and eventually extract them.
-
-If you need to share functionality between routes, place that
-functionality into the `plugins` folder, and share it via
-[decorators](https://fastify.dev/docs/latest/Reference/Decorators/).
-
-If you're a bit confused about using `async/await` to write routes, you would
-better take a look at [Promise resolution](https://fastify.dev/docs/latest/Reference/Routes/#promise-resolution) for more details.
diff --git a/typedoc.json b/typedoc.json
new file mode 100644
index 0000000..75da7d4
--- /dev/null
+++ b/typedoc.json
@@ -0,0 +1,13 @@
+{
+ "entryPointStrategy": "expand",
+ "entryPoints": [
+ "src"
+ ],
+ "exclude": [
+ "**/test/**",
+ "**/__tests__/**"
+ ],
+ "hideGenerator": true,
+ "out": "docs/api",
+ "tsconfig": "tsconfig.json"
+}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 79913db..57ecf96 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -576,6 +576,19 @@ __metadata:
languageName: node
linkType: hard
+"@gerrit0/mini-shiki@npm:^3.17.0":
+ version: 3.21.0
+ resolution: "@gerrit0/mini-shiki@npm:3.21.0"
+ dependencies:
+ "@shikijs/engine-oniguruma": "npm:^3.21.0"
+ "@shikijs/langs": "npm:^3.21.0"
+ "@shikijs/themes": "npm:^3.21.0"
+ "@shikijs/types": "npm:^3.21.0"
+ "@shikijs/vscode-textmate": "npm:^10.0.2"
+ checksum: 10c0/4045d19854abfa4515381a04af07096c1de07471b029ee090375652d0199ed3fed6165a22bd9f8e8250c609124d8c05f5d4604eb6de87cf13513aa89cfb8d14e
+ languageName: node
+ linkType: hard
+
"@humanfs/core@npm:^0.19.1":
version: 0.19.1
resolution: "@humanfs/core@npm:0.19.1"
@@ -785,6 +798,51 @@ __metadata:
languageName: node
linkType: hard
+"@shikijs/engine-oniguruma@npm:^3.21.0":
+ version: 3.21.0
+ resolution: "@shikijs/engine-oniguruma@npm:3.21.0"
+ dependencies:
+ "@shikijs/types": "npm:3.21.0"
+ "@shikijs/vscode-textmate": "npm:^10.0.2"
+ checksum: 10c0/cb17c034b04e1333f90f267081b7fac0b53e56031f7d067723363a72cdbdf79e567dea216bbcae38a6d4b910570c2dd60a953ca941f4834768c0bb721131af5f
+ languageName: node
+ linkType: hard
+
+"@shikijs/langs@npm:^3.21.0":
+ version: 3.21.0
+ resolution: "@shikijs/langs@npm:3.21.0"
+ dependencies:
+ "@shikijs/types": "npm:3.21.0"
+ checksum: 10c0/79cfc2b8ac1f5c938bfb18db6233f86ca96948970068c2cc94559e30abac2036c35a2ae52015d07f72b6decfd6b2ae86321f9547ae0f994b6131e362781fbf1f
+ languageName: node
+ linkType: hard
+
+"@shikijs/themes@npm:^3.21.0":
+ version: 3.21.0
+ resolution: "@shikijs/themes@npm:3.21.0"
+ dependencies:
+ "@shikijs/types": "npm:3.21.0"
+ checksum: 10c0/f128a874231d84d93e16f347557e844c2b6493b41196b52e36a79874598abe2dbf3ee981dfe52dd09f8d7e21ed4ff41ab03c28de7a21313d9a0b691fbd3690c0
+ languageName: node
+ linkType: hard
+
+"@shikijs/types@npm:3.21.0, @shikijs/types@npm:^3.21.0":
+ version: 3.21.0
+ resolution: "@shikijs/types@npm:3.21.0"
+ dependencies:
+ "@shikijs/vscode-textmate": "npm:^10.0.2"
+ "@types/hast": "npm:^3.0.4"
+ checksum: 10c0/a86038c7ad10bb8104ea07cfa0dddf1e0646cf3b70a382978939c6144b21e5891395f5e705b7393476320f6196d86c6d8cd7ad6b3e1b356eb6a7e40c298c98f3
+ languageName: node
+ linkType: hard
+
+"@shikijs/vscode-textmate@npm:^10.0.2":
+ version: 10.0.2
+ resolution: "@shikijs/vscode-textmate@npm:10.0.2"
+ checksum: 10c0/36b682d691088ec244de292dc8f91b808f95c89466af421cf84cbab92230f03c8348649c14b3251991b10ce632b0c715e416e992dd5f28ff3221dc2693fd9462
+ languageName: node
+ linkType: hard
+
"@sinclair/typebox@npm:^0.34.41":
version: 0.34.41
resolution: "@sinclair/typebox@npm:0.34.41"
@@ -886,6 +944,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/hast@npm:^3.0.4":
+ version: 3.0.4
+ resolution: "@types/hast@npm:3.0.4"
+ dependencies:
+ "@types/unist": "npm:*"
+ checksum: 10c0/3249781a511b38f1d330fd1e3344eed3c4e7ea8eff82e835d35da78e637480d36fad37a78be5a7aed8465d237ad0446abc1150859d0fde395354ea634decf9f7
+ languageName: node
+ linkType: hard
+
"@types/http-errors@npm:*":
version: 2.0.5
resolution: "@types/http-errors@npm:2.0.5"
@@ -1730,6 +1797,13 @@ __metadata:
languageName: node
linkType: hard
+"entities@npm:^4.4.0":
+ version: 4.5.0
+ resolution: "entities@npm:4.5.0"
+ checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250
+ languageName: node
+ linkType: hard
+
"env-paths@npm:^2.2.1":
version: 2.2.1
resolution: "env-paths@npm:2.2.1"
@@ -2789,6 +2863,15 @@ __metadata:
languageName: node
linkType: hard
+"linkify-it@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "linkify-it@npm:5.0.0"
+ dependencies:
+ uc.micro: "npm:^2.0.0"
+ checksum: 10c0/ff4abbcdfa2003472fc3eb4b8e60905ec97718e11e33cca52059919a4c80cc0e0c2a14d23e23d8c00e5402bc5a885cdba8ca053a11483ab3cc8b3c7a52f88e2d
+ languageName: node
+ linkType: hard
+
"locate-path@npm:^3.0.0":
version: 3.0.0
resolution: "locate-path@npm:3.0.0"
@@ -2962,6 +3045,13 @@ __metadata:
languageName: node
linkType: hard
+"lunr@npm:^2.3.9":
+ version: 2.3.9
+ resolution: "lunr@npm:2.3.9"
+ checksum: 10c0/77d7dbb4fbd602aac161e2b50887d8eda28c0fa3b799159cee380fbb311f1e614219126ecbbd2c3a9c685f1720a8109b3c1ca85cc893c39b6c9cc6a62a1d8a8b
+ languageName: node
+ linkType: hard
+
"makeerror@npm:1.0.12":
version: 1.0.12
resolution: "makeerror@npm:1.0.12"
@@ -2971,6 +3061,22 @@ __metadata:
languageName: node
linkType: hard
+"markdown-it@npm:^14.1.0":
+ version: 14.1.0
+ resolution: "markdown-it@npm:14.1.0"
+ dependencies:
+ argparse: "npm:^2.0.1"
+ entities: "npm:^4.4.0"
+ linkify-it: "npm:^5.0.0"
+ mdurl: "npm:^2.0.0"
+ punycode.js: "npm:^2.3.1"
+ uc.micro: "npm:^2.1.0"
+ bin:
+ markdown-it: bin/markdown-it.mjs
+ checksum: 10c0/9a6bb444181d2db7016a4173ae56a95a62c84d4cbfb6916a399b11d3e6581bf1cc2e4e1d07a2f022ae72c25f56db90fbe1e529fca16fbf9541659dc53480d4b4
+ languageName: node
+ linkType: hard
+
"mdast-util-from-markdown@npm:^2.0.0":
version: 2.0.1
resolution: "mdast-util-from-markdown@npm:2.0.1"
@@ -3000,6 +3106,13 @@ __metadata:
languageName: node
linkType: hard
+"mdurl@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "mdurl@npm:2.0.0"
+ checksum: 10c0/633db522272f75ce4788440669137c77540d74a83e9015666a9557a152c02e245b192edc20bc90ae953bbab727503994a53b236b4d9c99bdaee594d0e7dd2ce0
+ languageName: node
+ linkType: hard
+
"media-typer@npm:^1.1.0":
version: 1.1.0
resolution: "media-typer@npm:1.1.0"
@@ -3300,7 +3413,7 @@ __metadata:
languageName: node
linkType: hard
-"minimatch@npm:^9.0.4":
+"minimatch@npm:^9.0.4, minimatch@npm:^9.0.5":
version: 9.0.5
resolution: "minimatch@npm:9.0.5"
dependencies:
@@ -3820,6 +3933,13 @@ __metadata:
languageName: node
linkType: hard
+"punycode.js@npm:^2.3.1":
+ version: 2.3.1
+ resolution: "punycode.js@npm:2.3.1"
+ checksum: 10c0/1d12c1c0e06127fa5db56bd7fdf698daf9a78104456a6b67326877afc21feaa821257b171539caedd2f0524027fa38e67b13dd094159c8d70b6d26d2bea4dfdb
+ languageName: node
+ linkType: hard
+
"punycode@npm:^2.1.0, punycode@npm:^2.3.0":
version: 2.3.1
resolution: "punycode@npm:2.3.1"
@@ -4194,6 +4314,7 @@ __metadata:
openid-client: "npm:^6.8.1"
prettier: "npm:^3.7.4"
prettier-plugin-jsdoc: "npm:^1.7.0"
+ typedoc: "npm:^0.28.16"
typescript: "npm:^5.9.3"
typescript-eslint: "npm:^8.48.1"
languageName: unknown
@@ -4323,6 +4444,23 @@ __metadata:
languageName: node
linkType: hard
+"typedoc@npm:^0.28.16":
+ version: 0.28.16
+ resolution: "typedoc@npm:0.28.16"
+ dependencies:
+ "@gerrit0/mini-shiki": "npm:^3.17.0"
+ lunr: "npm:^2.3.9"
+ markdown-it: "npm:^14.1.0"
+ minimatch: "npm:^9.0.5"
+ yaml: "npm:^2.8.1"
+ peerDependencies:
+ typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x
+ bin:
+ typedoc: bin/typedoc
+ checksum: 10c0/ae444913068088e88be6319a017a3a18f69cbd91dbb5b959fbdd0cf87d1a2a07f3a0d4ab29c957a83dd72808ff35bdd6ceec3ad1803fa412ddceffb78fa60ebb
+ languageName: node
+ linkType: hard
+
"typescript-eslint@npm:^8.48.1":
version: 8.48.1
resolution: "typescript-eslint@npm:8.48.1"
@@ -4358,6 +4496,13 @@ __metadata:
languageName: node
linkType: hard
+"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "uc.micro@npm:2.1.0"
+ checksum: 10c0/8862eddb412dda76f15db8ad1c640ccc2f47cdf8252a4a30be908d535602c8d33f9855dfcccb8b8837855c1ce1eaa563f7fa7ebe3c98fd0794351aab9b9c55fa
+ languageName: node
+ linkType: hard
+
"undici-types@npm:~6.21.0":
version: 6.21.0
resolution: "undici-types@npm:6.21.0"
@@ -4516,6 +4661,15 @@ __metadata:
languageName: node
linkType: hard
+"yaml@npm:^2.8.1":
+ version: 2.8.2
+ resolution: "yaml@npm:2.8.2"
+ bin:
+ yaml: bin.mjs
+ checksum: 10c0/703e4dc1e34b324aa66876d63618dcacb9ed49f7e7fe9b70f1e703645be8d640f68ab84f12b86df8ac960bac37acf5513e115de7c970940617ce0343c8c9cd96
+ languageName: node
+ linkType: hard
+
"yargs-parser@npm:^21.1.1":
version: 21.1.1
resolution: "yargs-parser@npm:21.1.1"