Convention-over-configuration mock server for Bun.
- Mock only the endpoints you need and proxy everything else.
- Keep mocks as files instead of custom middleware glue.
- Add dynamic values with templates, helpers, and hooks.
Read more: docs/README.md
- Bun ≥ 1.1
- An empty directory (or an existing project) where smocky should live
Inside any project:
bun add -d github:YOUR_USER/smocky#v0.1.0npm publishing is planned. Until then, install from a tagged GitHub release and pin the tag (e.g.
#v0.1.0) for reproducible installs.
The fastest way to bootstrap is to let smocky init write the files
for you. Two flavors:
Interactive (recommended for first-time use):
bun smocky initYou'll be asked for a project name, port, and which optional pieces to
include (example endpoints, a helpers/ folder, a seeded db/, a
tsconfig.json). Answer the prompts and you'll end up with:
.
├── smocky.config.ts
├── endpoints/
│ ├── health/response.json
│ ├── users/response.json
│ └── users/_id/response.json
└── helpers/guid.ts # if you opted in
Non-interactive (CI, scripts):
bun smocky init --yes --name my-api --port 3000 --examples --helpersFrom an OpenAPI spec:
# Local file
bun smocky init --from-openapi ./openapi.yaml
# Remote, with auth
bun smocky init --from-openapi https://api.example.com/openapi.json \
--header "Authorization: Bearer $TOKEN"This generates one endpoints/<path>/response.json per operation in the
spec, merging multiple methods (GET/POST/DELETE) into a single
file per folder. Re-running is additive: missing methods get added,
existing ones are preserved (use --force to overwrite).
Full reference: docs/features/init.md.
bun smocky serveYou should see a startup banner and the server begins listening on
http://localhost:3000. Hit your mocks with curl:
curl localhost:3000/health
# {"ok":true,"service":"my-api"}
curl localhost:3000/users/42
# {"id":"42","name":"User 42"}If you configured a baseUrl in smocky.config.ts, anything you
haven't mocked is transparently proxied to the real backend.
Read the Folder Conventions for the rules, then dive into Templating, Hooks, or the Database when you need more.
Step-by-step walkthrough that builds the same scaffold by hand:
docs/getting-started.md.
If you cloned this repo (rather than installing the package):
bun install
bun run dev # equivalent to: bun run src/cli/index.ts serveThe bundled example smocky.config.ts proxies unmocked routes to
https://jsonplaceholder.typicode.com, so you can immediately try:
curl localhost:3000/users # mocked
curl localhost:3000/posts # proxied.
├── smocky.config.ts
├── endpoints/
│ └── users/
│ ├── response.json
│ └── _id/
│ ├── hook.ts
│ └── response.json
└── helpers/
├── guid.ts
└── randomInt.ts
endpoints/<path>/response.jsondefines method responses._paramfolders become dynamic route params.hook.tsmutates the response after templating.helpers/*.tsare callable from{{ ... }}tokens.
Read more: docs/features/conventions.md
Static and multi-method mock:
{
"GET": {
"status": 200,
"body": {
"users": [
{ "id": "{{ guid }}", "name": "Alice" },
{ "id": "{{ guid }}", "name": "Bob" }
]
}
},
"POST": {
"status": 201,
"body": {
"id": "{{ guid }}",
"echo": "{{ req.body.name }}"
}
}
}Dynamic route example:
{
"GET": {
"body": {
"id": "{{ req.params.id }}",
"name": "User-{{ req.params.id }}",
"luckyNumber": "{{ randomInt 1 100 }}"
}
}
}Read more: docs/features/routing.md
{{ req.params.id }}reads request data.{{ guid }}calls a helper and preserves its return type when it is the whole string.user-{{ req.params.id }}stringifies and embeds the value.{{ randomInt 1 100 }}passes helper arguments as strings.
Read more: docs/features/templating.md
// helpers/guid.ts
export default function guid(): string {
return crypto.randomUUID();
}// helpers/randomInt.ts
export default function randomInt(min: string, max: string): number {
const lo = Number(min);
const hi = Number(max);
if (Number.isNaN(lo) || Number.isNaN(hi)) {
throw new Error(`randomInt: invalid bounds "${min}" / "${max}"`);
}
return Math.floor(Math.random() * (hi - lo + 1)) + lo;
}Read more: docs/features/helpers.md
import type { Hook } from 'smocky';
const hook: Hook = (req, res) => {
if (req.params.id === '404') {
res.status = 404;
res.body = { error: 'not found', id: req.params.id };
}
res.headers['x-hooked'] = 'true';
};
export default hook;Use a hook when you need conditional logic or response mutation that would be awkward in response.json alone.
Read more: docs/features/hooks.md
Smocky ships with a small in-memory DB seeded from db/*.json.
// db/users.json
[
{ "id": "u1", "name": "Alice", "active": true },
{ "id": "u2", "name": "Bob", "active": false }
]Templates can read from the DB:
{
"GET": {
"body": {
"users": "{{ db.users.all }}",
"activeUsers": "{{ db.users.where active=true }}"
}
}
}Hooks can mutate it:
import type { Hook } from 'smocky';
const hook: Hook = (req, res, ctx) => {
const users = ctx.db!.collection('users');
if (req.method === 'POST') {
res.body = users.insert(req.body as { name: string; active?: boolean });
res.status = 201;
}
};
export default hook;Typical flow:
curl localhost:3000/users
curl -X POST localhost:3000/users -H 'content-type: application/json' -d '{"name":"Cara","active":true}'
curl localhost:3000/usersPersistence stays opt-in through smocky.config.ts -> db.persist.
Read more: docs/features/database.md
Enable recording for a run:
bun smocky serve --recordOr set it in smocky.config.ts:
record: {
enabled: true,
include: ['/api/'],
exclude: ['/health'],
overwrite: false,
}Only JSON upstream responses are recorded.
Read more: docs/features/proxy-and-recorder.md
import { defineConfig } from 'smocky';
export default defineConfig({
port: 3000,
baseUrl: 'https://jsonplaceholder.typicode.com',
endpointsDir: './endpoints',
helpersDir: './helpers',
globalHeaders: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': '*',
},
record: {
enabled: false,
outputDir: './endpoints',
include: [],
exclude: [],
overwrite: false,
},
db: {
dir: './db',
persist: false,
autoId: 'uuid',
},
});Environment overrides:
PORTBASE_URLRECORD=1
Read more: docs/reference/configuration.md
import { Smocky, startServer } from 'smocky';
// Quick start with startServer
const server = await startServer({ port: 4000 });
await server.stop();
// Full control with the Smocky class (workspace support)
const instance = await Smocky.start({
port: 4000,
workspace: 'user-tests',
});
await instance.stop();Read more: docs/reference/api.md
smocky serve [--config <path>] [--port <n>] [--base-url <url>] [--record]
smocky check api [--fail] [--base-url <url>]
smocky check mocks [--fail]
smocky check all [--fail] [--base-url <url>]
smocky init [--yes] [--name <s>] [--port <n>]
[--examples|--no-examples] [--helpers|--no-helpers]
[--db|--no-db] [--tsconfig|--no-tsconfig]
[--cwd <dir>] [--force]
smocky init --from-openapi <spec> [--header "Name: value" ...]
[--yes] [--cwd <dir>] [--force]
Common flags:
--config <path>— alternatesmocky.config.tslocation.--port <n>— override the configured port.--base-url <url>— applies to bothserveandcheck, so the checker can target a different backend without editing config.--record— record proxied responses as new mocks.--fail—checkexits with status 1 on any drift.--help,--version— standard.
Read more: docs/features/init.md · docs/reference/api.md
Smocky ships with a CLI checker that compares your OpenAPI spec against:
- the local mocks under
endpoints/ - the real backend at
baseUrl
Quick start:
bun smocky check mocks
bun smocky check api --base-url http://127.0.0.1:3000
bun smocky check all --fail --base-url http://127.0.0.1:3000The bundled example config points baseUrl at https://jsonplaceholder.typicode.com, which does not match the bundled /users spec. For a green end-to-end example, run the local server and point check api back at smocky itself:
bun smocky serve --base-url ""
bun smocky check api --base-url http://127.0.0.1:3000Sample overrides live in openapi.check.sampleData and are keyed by either operationId or <METHOD> <path>:
{
"POST /users": {
"name": "Example User",
"active": true
},
"createUser": {
"name": "Example User",
"active": true
}
}You can skip endpoints with openapi.check.skipPaths:
openapi: {
spec: './examples/openapi.json',
check: {
skipPaths: ['/internal/', /^\/health/],
},
}Output is text-only in v1.
Read more: docs/features/openapi-checker.md
Full docs live in docs/README.md.