| name | jarvus-fastify |
| description | Backend development using Fastify + TypeScript on Bun. Use when creating new backend APIs, adding routes, implementing services, working with plugins, or configuring environment variables. |
Backend Fastify Stack (Bun)
High-performance backend stack, run on Bun:
- Bun - Runtime, package manager, test runner, and watch-mode dev server (no Node.js, npm, or tsx)
- Fastify 5.x - Web framework
- TypeScript - Type safety, executed directly by Bun (no build step for services)
- pino-pretty - Pretty logging for development
- @fastify/env - Environment variable validation with JSON Schema
- @fastify/cors - CORS support
- fastify-plugin - Plugin system
Bun runs TypeScript source directly, so there is no compile step in the dev loop and
none in production for a service — you ship and run the source. tsc is kept only as a
type checker (tsc --noEmit), never as a build tool.
Environment Setup
Use asdf to manage Bun:
asdf plugin add bun
asdf set bun latest
asdf install
This writes a .tool-versions file pinning Bun so the whole team runs the same version.
A born-on-Bun service needs only bun in .tool-versions — add nodejs only if the
repo also ships an npm-distributed CLI that needs the Node toolchain.
Reference Files
Quick Reference
Commands
bun run dev
bun run typecheck
bun run start
bun test
Package Management
Use Bun for all dependency management — never edit package.json by hand:
bun add fastify-plugin @fastify/cors
bun add -d @types/bun pino-pretty
bun remove <pkg>
bun add resolves the latest compatible version and keeps bun.lock in sync. Commit
bun.lock.
Key Imports
import Fastify, { FastifyInstance, FastifyPluginAsync } from 'fastify'
import fp from 'fastify-plugin'
import fastifyEnv from '@fastify/env'
import cors from '@fastify/cors'
Configuration Access
Always access configuration through fastify.config, never process.env directly:
const port = fastify.config.PORT
const apiKey = fastify.config.API_KEY
const port = process.env.PORT
@fastify/env's JSON Schema covers per-variable shape validation. When config needs
cross-field contracts ("exactly one of these two modes"), environment-detection guards,
or strict tri-state booleans, hand-rolled fail-fast validation at boot is the right tool
— see setup-guide.md "When JSON Schema isn't enough".
Response Format
Pick one convention per API and stick to it. The envelope below is one option; structured
domain-specific bodies (e.g. {error: "forbidden", required, environment, your_role}) are
equally valid and often spec-mandated — see api-design.md.
{
success: boolean
data?: T
error?: string
metadata?: { timestamp: Date }
}
Plugin Pattern
import fp from 'fastify-plugin'
export default fp(async (fastify, opts) => {
fastify.decorate('something', value)
}, '5.x')
Route Pattern
import { FastifyPluginAsync } from 'fastify'
const routes: FastifyPluginAsync = async (fastify, opts) => {
fastify.get('/', async (request, reply) => {
return { success: true, data: 'example' }
})
}
export default routes
Service Pattern
export class MyService {
constructor(private fastify: FastifyInstance) {}
async doWork() {
const apiKey = this.fastify.config.API_KEY
this.fastify.log.info('Service method called')
}
}
declare module 'fastify' {
interface FastifyInstance {
myService: MyService
}
}
fastify.decorate('myService', new MyService(fastify))
TypeScript Config
Bun runs the source directly, so tsconfig.json is for type checking only
("noEmit": true). The Bun-native config:
{
"compilerOptions": {
"target": "ESNext",
"module": "Preserve",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"types": ["@types/bun"],
"strict": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true,
"noEmit": true
}
}
In a Bun-workspaces monorepo, the package tsconfigs use TypeScript project references
("composite": true, per-package references) and the root runs tsc -b; the runtime
story is unchanged — Bun still executes the source.
Project Structure
backend/
├── src/
│ ├── plugins/ # Fastify plugins (env, auth, etc.)
│ ├── routes/ # HTTP route handlers
│ ├── services/ # Business logic classes
│ ├── lib/ # Shared utilities / clients
│ ├── app.ts # Plugin registration & setup
│ └── index.ts # Server entry point
├── package.json
├── tsconfig.json
├── .env.example
└── .gitignore
Common Gotchas
- Plugin order matters: Register env plugin first, then services, then routes
- Config access: Use
fastify.config.VAR not process.env.VAR
- Server ready: Call
await server.ready() before accessing config in index.ts
- No build step: Bun runs the source;
tsc is --noEmit for type checking only
- Path normalization: Centralize path utilities, handle root '/' as special case
- Package management: Use
bun add <pkg> not manual package.json edits
CI & Code Quality
Wire lint, format, and type-check into CI from the start. The cross-cutting setup (asdf
provisioning + caching, path-filtered workflows, lockfile-frozen installs, the GitHub Actions
templates) lives in the ci-quality-gates skill; this section is the Fastify-stack
specifics that plug into it.
Linter + formatter: oxlint + oxfmt (the Jarvus standard — not eslint/prettier):
bun add -d oxlint oxfmt
The script contract. Expose the same four scripts every Jarvus TS package does, so CI
just calls bun run <name>:
{
"scripts": {
"dev": "bun --watch src/index.ts",
"start": "bun run src/index.ts",
"typecheck": "tsc --noEmit",
"lint": "oxlint index.ts src",
"format": "oxfmt index.ts src",
"format:check": "oxfmt --check index.ts src",
"test": "bun test"
}
}
Use the base oxc config (references/templates/oxlintrc.base.json from ci-quality-gates,
correctness=error) as the service's .oxlintrc.json — backend code doesn't need the React
tier. In a Bun-workspaces monorepo, put the base config at the repo root as
.oxlintrc.base.json and have each package extend it; a service that contains a UI subfolder
adds "ignorePatterns": ["ui"] so the UI is linted by its own config.
CI workflow. A single service uses one ts-lint-style job (lint + format:check +
typecheck) plus a test job; a monorepo uses the per-package matrix in ci-quality-gates's
lint.yml / test.yml. All provisioned by the shared setup-asdf composite. Type-check is a
required gate — tsc --noEmit catches bugs no linter does.