apifacility
v0.2.6
Published
Build & deploy real APIs from a JSON spec — composable packs, auth, DB, WebSocket, all declarative. One command.
Maintainers
Readme
🚀 Apideploy
👋 Hello, I'm a maintainer! Apideploy is a submodule of apibooks — same family, same goal: ship real APIs faster. Where apibooks documents your API, Apideploy builds it.
📘 Full usage guide & code examples: jump to the Usage Guide ↓
Build and run a real API (REST + WebSocket + auth + DB) from a single JSON file. You compose packs instead of writing code.
npm install -g @apifacility/apideploy
apideploy init # creates apideploy.json
apideploy start # API live on http://localhost:3000👋 By the maintainer · Par le mainteneur
🇬🇧 Hi, I'm Rayan Chattaoui — creator and active maintainer of apifacility / apideploy. It's the "build" piece of a small open-source family I develop and maintain, all designed to work together:
- 🚀 apifacility — build real APIs from a JSON spec
- 📘 apibooks — auto-generate beautiful API docs
- 🤖 apiupbot — distributed uptime monitoring
⭐ Star it, install it, and tell me what you need next.
🇫🇷 Bonjour, je suis Rayan Chattaoui — créateur et mainteneur actif d'apifacility / apideploy. C'est la brique « build » d'une petite famille open-source que je développe et maintiens, pensée pour fonctionner ensemble (apifacility + apibooks + apiupbot). ⭐ Mets une étoile, installe, et dis-moi ce qu'il te faut ensuite.
🆕 What's new · Nouveautés (v0.2.x)
Early, actively-developed releases — the ecosystem is in alpha. · Versions récentes, en développement actif — l'écosystème est en alpha.
🇬🇧 Latest capabilities:
- 🧩 Composable packs — add
auth.jwt,db.sqlite/mongodb/postgres,ws,qrcodeand more without writing code - 🛒 Community pack hub —
search,add-pack,publish,vote, all packs run sandboxed - 📥 OpenAPI import — generate a spec straight from an existing
openapi.json - 🔗 Ecosystem interop — now pairs natively with apibooks (docs) and apiupbot (monitoring)
🇫🇷 Dernières capacités :
- 🧩 Packs composables — ajoutez
auth.jwt,db.sqlite/mongodb/postgres,ws,qrcode, etc. sans écrire de code - 🛒 Hub de packs communautaires —
search,add-pack,publish,vote, packs exécutés en sandbox - 📥 Import OpenAPI — génère une spec directement depuis un
openapi.jsonexistant - 🔗 Interopérabilité — s'associe nativement à apibooks (doc) et apiupbot (monitoring)
🧠 The idea
An Apideploy API = a JSON file that maps endpoints to packs.
A pack = one .js file wrapping an npm library (one per capability: auth.jwt, db.sqlite, qrcode, etc.).
- A technical user writes a pack once (~20 lines).
- Anyone composes endpoints in JSON — no application code.
{
"routes": {
"POST /qr": { "use": "qrcode" },
"POST /pdf": { "use": "pdf-create" },
"POST /slug": { "use": "slugify" }
}
}Run apideploy start → 3 working endpoints, instantly.
⚡ Express creation: 1 line per endpoint
apideploy packs # list available packs
apideploy add /qr qrcode # add POST /qr (pack qrcode)
apideploy add /pdf pdf-create # add POST /pdf (pack pdf-create)
apideploy start # boom, API live📥 Use ANY npm package — auto-generated packs
Don't have a pack for the library you need? Generate one from any npm package:
apideploy install slugify # → packs/auto/slugify.js
apideploy install js-yaml # → { "use": "js-yaml", "op": "load" }
apideploy install validator --as v # custom aliasApideploy introspects the package (function / class / object of functions) and
scaffolds the matching pack into packs/auto/, tracked in _registry.json.
Auto-generated packs run sandboxed (see Security below).
🧩 Composition: chain several packs on one endpoint
Sequential pipeline (array) — each step's output feeds the next
"POST /signup": [
{ "use": "validate", "schema": { "email": "email", "password": "string!" } },
{ "use": "hash-password", "input": "body.password" },
{ "use": "db.sqlite", "op": "insert", "table": "users",
"data": { "email": "{{body.email}}", "password": "{{_prev}}" } }
]Named steps (object pipeline) — to cross-reference results
"POST /order": {
"pipeline": {
"product": { "use": "db.sqlite", "op": "findOne", "table": "products",
"where": { "id": "{{body.productId}}" } },
"order": { "use": "db.sqlite", "op": "insert", "table": "orders",
"data": { "userId": "{{auth.userId}}", "productId": "{{product.id}}", "price": "{{product.price}}" } },
"_response": { "ok": true, "orderId": "{{order.id}}", "product": "{{product.name}}" }
}
}Any step can reference any previous step via {{stepName.field}}.
🔐 Auth (JWT, bearer, API key)
Root auth block + per-route override:
{
"auth": { "type": "jwt", "secret": "env:JWT_SECRET" },
"routes": {
"GET /me": { "use": "db.sqlite", "op": "findOne", "table": "users",
"where": { "id": "{{auth.userId}}" } },
"POST /login": { "auth": false, "pipeline": { /* ... */ } },
"POST /signup": { "auth": false, "pipeline": { /* ... */ } }
}
}→ Every route is protected except those with "auth": false. The decoded JWT payload is exposed via {{auth.X}}.
3 auth packs shipped: auth.jwt, auth.bearer, auth.api-key.
🗄️ Database (SQLite / MongoDB / Postgres)
{
"db": {
"type": "sqlite",
"path": "./app.db",
"schema": {
"users": { "id": "INTEGER PRIMARY KEY AUTOINCREMENT", "email": "TEXT UNIQUE", "password": "TEXT" },
"products": { "id": "INTEGER PRIMARY KEY AUTOINCREMENT", "name": "TEXT", "price": "REAL" }
}
}
}Tables are created automatically at startup. The db.sqlite pack exposes:
op: "insert"→step.table,step.data(or the input)op: "findOne"→step.table,step.whereop: "find"→step.table,step.where,step.limitop: "update"→step.table,step.where,step.dataop: "delete"→step.table,step.whereop: "query"→step.sql,step.params(raw SQL)
Same operations on db.mongo and db.postgres.
🌐 WebSocket
Same pipeline grammar, inside a ws block:
{
"ws": {
"/chat": {
"auth": { "type": "jwt", "input": "query.token" },
"onConnect": { "use": "ws.join", "channel": "chat" },
"onMessage": [
{ "use": "validate", "schema": { "text": "string!" } },
{ "use": "db.sqlite", "op": "insert", "table": "messages",
"data": { "userId": "{{auth.userId}}", "text": "{{body.text}}" } },
{ "use": "ws.broadcast", "channel": "chat",
"message": { "from": "{{auth.userId}}", "text": "{{body.text}}" } }
],
"onDisconnect": { "use": "ws.leave" }
}
}
}→ Handshake authentication (rejects 401), DB persistence, broadcast — all in one file.
4 WS packs: ws.broadcast, ws.join, ws.leave, ws.send.
🌍 Community packs
Validated packs from the community hub — distributed as readable .js, never via npm.
apideploy search yaml # browse the registry
apideploy add-pack reverse # download + verify sha256 + install (sandboxed)
apideploy publish ./my-pack # validate + open a PR to the hub
apideploy vote reverse # 👍 to promote a packA pack becomes a base pack only after both enough community votes and
a maintainer security sign-off (status: listed → candidate → verified). Hub:
github.com/Aiglator/apideploy-packs.
🛡️ Security — trust tiering, sandbox & limitations
Running arbitrary npm code is powerful and risky. Apideploy defends it with three layers: a static validator, a runtime sandbox, and human sign-off.
Trust tiers — who runs where
| Tier | Packs | Execution |
| --- | --- | --- |
| core / local | shipped with Apideploy, or your own via --dir | in-process (trusted — you wrote/vetted them) |
| auto | apideploy install <npm-pkg> | sandboxed |
| community | apideploy add-pack <name> | sandboxed |
Only auto and community packs are treated as untrusted. Core/local packs run
in-process for speed — so only point --dir at code you trust.
Layer 1 — static validator (apideploy validate)
Runs at publish time, in the hub CI, and on demand. It parses the pack (real
AST pass via acorn when available, lexical scan otherwise) and rejects a
pack that:
- doesn't export
{ name, run }; - uses
eval()ornew Function(); - uses a dynamic
require(variable)(only string literals allowed); - requires a forbidden builtin:
child_process,cluster,worker_threads,vm,v8,inspector,repl,module,process; - requires a network builtin (
net,tls,http,https,http2,dgram,dns) without a declarednetcapability; - requires
fswithout a declaredfscapability; - requires a 3rd-party module not declared in
pack.jsondeps.
Reading process.env.X without declaring it raises a warning.
Layer 2 — runtime sandbox
Untrusted packs run in a separate child_process.fork under Node's
Permission Model (--permission --allow-fs-read=*), plus in-process
hardening applied before the pack loads:
| Control | Default (deny-all) |
| --- | --- |
| Spawn processes (child_process, cluster, worker_threads) | ❌ blocked |
| Escapes (vm, v8, inspector, repl, module) | ❌ blocked |
| eval() | ❌ disabled |
| Filesystem write | ❌ denied by the OS permission model |
| Network (net/http/https/…) | ❌ blocked unless net capability |
| require('fs') (reads) | ❌ blocked unless fs capability |
| Native addons | ❌ blocked unless addons capability |
| process.env | 🔒 stripped to only the declared env keys |
| Wall-clock time | ⏱ 5 s then SIGKILL |
| Memory | 🧠 128 MB cap (--max-old-space-size) |
Granting a capability (opt-in)
Capabilities are declared in the pack's pack.json. Nothing is granted
implicitly — you ask for exactly what the pack needs:
{
"name": "fetch-quote",
"deps": { "node-fetch": "^3.0.0" },
"capabilities": {
"net": ["api.example.com"], // re-enables network builtins (host list = intent, reviewed at publish)
"fs": false, // true → allow require('fs') for reads
"env": ["QUOTE_API_TOKEN"], // only these env vars reach the pack
"addons": false // true → allow native addons
}
}Example — a refused pack
// bad-pack.js
const cp = require('child_process'); // ❌ forbidden builtin
module.exports = { name: 'oops', run: () => cp.execSync('whoami').toString() };$ apideploy validate bad-pack.js
validateur: ast requires: child_process
✗ 1 erreur(s):
- forbidden builtin require('child_process')At install time, apideploy add-pack re-runs the validator, verifies the
sha256 against the registry, shows the requested capabilities, and refuses
non-verified packs unless you pass --force.
⚠️ Real limitations — read this before trusting the sandbox
- Node ≥ 20 required for the OS-level jail. The Permission Model is stable on
Node ≥ 22, experimental on ≥ 20. On older Node, there is no kernel-enforced
sandbox — only the in-process hardening (require guard, env strip,
evaloff), which a determined attacker may bypass. A warning is printed when this happens. - Filesystem reads are allowed (
--allow-fs-read=*) so packs can load their dependencies. A sandboxed pack with thefscapability can read files it has OS permission to; only writes are denied. - The
nethost list is not egress-filtered at runtime. Declaringnetre-enables network modules; the host array documents intent and is checked by reviewers, but Node does not enforce per-host network rules here. - In-process hardening is defense-in-depth, not a perfect jail. The robust isolation target (container / microVM) is the hosted phase-2 runtime.
Bottom line: treat sandboxed packs as reduced-trust, not zero-trust. Run
untrusted packs on Node ≥ 22, review the capabilities block before installing,
and never expose secrets through env vars a pack can declare.
🔧 CLI
apideploy start [file] Launch the server (default: apideploy.json)
apideploy init [file] Create a starter spec
apideploy packs List every available pack
apideploy install <npm-pkg> Auto-generate a pack from any npm package
apideploy add [METHOD] <path> <pk> Add a route in one command
apideploy from-openapi <file> Generate a spec from OpenAPI
Community:
apideploy search [keyword] Search the pack registry
apideploy add-pack <name> Install a community pack (sandboxed)
apideploy remove-pack <name> Uninstall a community pack
apideploy publish <dir|pack.js> Validate + prepare a hub PR
apideploy validate <pack> Run the security validator
apideploy vote <name> Vote to promote a pack
apideploy registry [--set <url>] Show / switch the registry backendstart options:
-p, --port <n>port (default3000)-d, --delay <ms>simulated latency-w, --watchhot-reload on file change--no-corsdisable CORS--no-verbosesuppress request logs
🎨 Template tokens
Available anywhere in string values of the spec:
| Token | Value |
| --------------------- | ---------------------------------------------- |
| {{params.id}} | path param (/users/:id) |
| {{query.page}} | query param (?page=2) |
| {{body.email}} | JSON body field |
| {{headers.x-thing}} | HTTP header |
| {{auth.userId}} | payload returned by the auth pack |
| {{_prev}} | previous step output (sequential pipeline) |
| {{stepName.field}} | output of a named step |
| {{$uuid}}, {{$name}}, {{$email}}, {{$int 1 100}}, {{$date}} | random fake data |
| env:NAME | environment variable (resolved at startup) |
📦 Write your own pack
A pack = packs/<name>.js:
module.exports = {
name: 'uppercase',
description: 'Convert text to UPPERCASE',
defaultInput: 'body.text',
// optional — called once at startup if present
init: (config) => { /* open a connection, read a secret, ... */ },
// called on each request
run: async (input, context) => ({ result: String(input).toUpperCase() })
};Subfolders = namespace: packs/db/sqlite.js → pack db.sqlite.
📥 Optional dependencies (peer deps)
Apideploy's only hard dependency is ws (required for WebSocket). The rest install à la carte depending on the packs you use:
| If you use… | Install |
| ----------------------- | ---------------------- |
| auth.jwt | npm i jsonwebtoken |
| db.sqlite | npm i better-sqlite3 |
| db.mongo | npm i mongodb |
| db.postgres | npm i pg |
| hash-password | npm i bcryptjs |
| qrcode | npm i qrcode |
| pdf-create, pdf-info| npm i pdf-lib |
If a dependency is missing, the pack still appears in apideploy packs but throws a clear error at runtime.
📘 Usage Guide
A step-by-step walkthrough with copy-paste examples.
Contents: Install · Quick start · Spec anatomy · Route types · Fake data & tokens · Pipelines · Auth · Database · WebSocket · CLI reference · Write a pack · Recipes
Install
# Global install (recommended for the CLI)
npm install -g @apifacility/apideploy
# or locally in a project
npm install @apifacility/apideployNode.js 14+ is required. The only hard dependency is ws (WebSocket). The
other libraries (JWT, SQLite, bcrypt, qrcode…) install à la carte depending on
the packs you use.
Quick start
apideploy init # creates a sample apideploy.json
apideploy start # API live on http://localhost:3000init generates an apideploy.json with CRUD routes on /users. Try it:
curl http://localhost:3000/api/users
curl http://localhost:3000/api/users/42
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"[email protected]"}'💡 The
/apiprefix comes from the spec'sbaseUrlfield.
Handy launch options:
apideploy start mocks.json -p 4000 # custom port
apideploy start mocks.json --watch # hot reload on every change
apideploy start mocks.json -d 300 # simulated 300 ms latency
apideploy start mocks.json --no-cors # disable CORS headersSpec anatomy
A spec file is JSON with a few top-level blocks:
{
"baseUrl": "/api", // prefix added to every route (optional)
"delay": 0, // global latency in ms (optional)
"cors": true, // CORS headers (optional, default: true)
"auth": { "...": "..." }, // global auth config (optional)
"db": { "...": "..." }, // database config (optional)
"routes": { "GET /users": { "...": "..." } }, // REST endpoints
"ws": { "/chat": { "...": "..." } } // WebSocket endpoints (optional)
}A route key follows the "METHOD /path" format:
"GET /users" // → GET /api/users
"POST /users/:id" // → POST /api/users/:id (:id = dynamic parameter)Route types
Apideploy detects a route's type from its shape.
a) Static / mocked response — the value is the body (status 200):
"GET /ping": { "message": "pong" },
"GET /tags": ["news", "tech", "sport"]b) Detailed response — control the HTTP code, headers or latency with the
reserved keys status, body, headers, delay:
"POST /users": {
"status": 201,
"delay": 200,
"headers": { "X-Created-By": "apideploy" },
"body": { "id": "{{$uuid}}", "name": "{{body.name}}", "created": "{{$date}}" }
},
"DELETE /users/:id": { "status": 204 }⚠️ A data field named
statusthat is non-numeric (e.g."status": "active") is treated as data, not as an HTTP code. A lone numericstatusis the HTTP code.
c) Pack-powered route — use names the pack to run; the response is its output:
"POST /qr": { "use": "qrcode" },
"GET /me": { "use": "db.sqlite", "op": "findOne", "table": "users",
"where": { "id": "{{auth.userId}}" } }d) Dynamic handler — in a .js spec, a route can be a function receiving the
request context (method, path, params, query, body, headers, auth):
// api.mock.js
module.exports = {
baseUrl: '/api',
routes: {
'GET /time': (ctx) => ({ now: Date.now(), path: ctx.path }),
'GET /users/:id': (ctx) => ({ status: 200, body: { id: ctx.params.id, q: ctx.query } })
}
};Fake data & tokens
Any string in the spec can contain {{ ... }} tokens, resolved per request.
| Token | Value |
| --------------------- | ---------------------------------------------- |
| {{params.id}} | path param (/users/:id) |
| {{query.page}} | query param (?page=2) |
| {{body.email}} | JSON body field |
| {{headers.x-token}} | HTTP header |
| {{auth.userId}} | payload returned by the auth pack |
| {{_prev}} | previous step's output (sequential pipeline) |
| {{stepName.field}} | a named step's output |
| env:NAME | environment variable (resolved at startup) |
Fake-data generators ($ prefix): {{$uuid}}, {{$id}}, {{$name}},
{{$firstname}}, {{$lastname}}, {{$email}}, {{$city}}, {{$int 18 65}},
{{$float 0 1}}, {{$bool}}, {{$date}}, {{$word}}, {{$sentence}},
{{$paragraph}}, {{$url}}, {{$phone}}, {{$pick a|b|c}}.
🎯 Type preserved: a string that is only a token (
"{{$int 1 100}}") injects the real typed value (the number42, not"42"). Mixed with text ("user-{{$int 1 9}}"), the result is a string.
"GET /posts": [
{ "id": "{{$int 1 999}}", "title": "{{$sentence}}", "author": "{{$name}}" }
],
"GET /search": {
"query": "{{query.q}}", "page": "{{query.page}}",
"results": [ { "id": "{{$uuid}}", "label": "{{$sentence}}" } ]
}Pipelines
Sequential (array) — each step's output feeds the next ({{_prev}}); the last
step's output is the response body:
"POST /signup": [
{ "use": "validate", "schema": { "email": "email", "password": "string!" } },
{ "use": "hash-password", "input": "body.password" },
{ "use": "db.sqlite", "op": "insert", "table": "users",
"data": { "email": "{{body.email}}", "password": "{{_prev}}" } }
]Named (object pipeline) — steps cross-reference via {{stepName.field}};
_response defines the body and _status the HTTP code:
"POST /order": {
"pipeline": {
"product": { "use": "db.sqlite", "op": "findOne", "table": "products",
"where": { "id": "{{body.productId}}" } },
"order": { "use": "db.sqlite", "op": "insert", "table": "orders",
"data": { "userId": "{{auth.userId}}", "productId": "{{product.id}}",
"price": "{{product.price}}" } },
"_response": { "ok": true, "orderId": "{{order.id}}", "product": "{{product.name}}" }
}
}Auth
A global auth block protects every route except those with "auth": false.
The decoded payload is exposed via {{auth.X}}.
{
"auth": { "type": "jwt", "secret": "env:JWT_SECRET" },
"routes": {
"POST /login": { "auth": false, "pipeline": { "...": "..." } },
"GET /me": { "use": "db.sqlite", "op": "findOne", "table": "users",
"where": { "id": "{{auth.userId}}" } }
}
}Mint a token in a login pipeline:
"token": { "use": "auth.jwt", "op": "sign",
"payload": { "userId": "{{user.id}}", "email": "{{user.email}}" } }Packs: auth.jwt (npm i jsonwebtoken), auth.bearer, auth.api-key.
Database
Declare a db block — tables are auto-created at startup
(use "path": ":memory:" for an ephemeral SQLite DB):
{
"db": {
"type": "sqlite", "path": "./app.db",
"schema": {
"users": { "id": "INTEGER PRIMARY KEY AUTOINCREMENT", "email": "TEXT UNIQUE", "password": "TEXT" }
}
}
}db.sqlite / db.mongo / db.postgres ops: insert · findOne · find ·
update · delete · query (raw SQL).
"GET /products": { "use": "db.sqlite", "op": "find", "table": "products" },
"GET /users/:id": { "use": "db.sqlite", "op": "findOne", "table": "users",
"where": { "id": "{{params.id}}" } }Deps: better-sqlite3, mongodb, or pg.
WebSocket
Same pipeline grammar inside a ws block, with lifecycle handlers onConnect,
onMessage, onDisconnect and an optional handshake auth (rejects with 401):
{
"ws": {
"/chat": {
"auth": { "type": "jwt", "input": "query.token" },
"onConnect": { "use": "ws.join", "channel": "chat" },
"onMessage": [
{ "use": "validate", "schema": { "text": "string!" } },
{ "use": "ws.broadcast", "channel": "chat",
"message": { "from": "{{auth.userId}}", "text": "{{body.text}}" } }
],
"onDisconnect": { "use": "ws.leave" }
}
}
}Client URL: ws://localhost:3000/api/chat?token=.... Packs: ws.broadcast,
ws.join, ws.leave, ws.send.
CLI reference
See the 🔧 CLI section above for the full command list. Build an API without hand-writing JSON:
apideploy packs # see available packs
apideploy add /qr qrcode # add POST /qr (pack qrcode)
apideploy install slugify # generate a pack from any npm package
apideploy start # it's liveWrite a pack
A pack is a packs/<name>.js file (subfolders namespace it: packs/db/sqlite.js
→ db.sqlite):
module.exports = {
name: 'uppercase',
description: 'Convert text to UPPERCASE',
defaultInput: 'body.text', // where to read input by default
init: (config) => { /* once at startup: DB connection, secret… */ },
run: async (input, context) => ({ result: String(input).toUpperCase() })
};context = { method, path, params, query, body, headers, auth, step, _prev }.
Point Apideploy at custom packs with --dir ./my-packs or packsDir in the spec.
Recipes
Mocked REST API for a front-end:
{
"baseUrl": "/api", "delay": 150,
"routes": {
"GET /users": [ { "id": "{{$uuid}}", "name": "{{$name}}", "email": "{{$email}}" } ],
"GET /users/:id": { "id": "{{params.id}}", "name": "{{$name}}", "email": "{{$email}}" },
"POST /users": { "status": 201, "body": { "id": "{{$uuid}}", "name": "{{body.name}}" } },
"DELETE /users/:id": { "status": 204 }
}
}Minimal full backend (auth + DB):
npm i jsonwebtoken better-sqlite3 bcryptjs
JWT_SECRET=dev-secret apideploy start app.json{
"auth": { "type": "jwt", "secret": "env:JWT_SECRET" },
"db": { "type": "sqlite", "path": ":memory:",
"schema": { "users": { "id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"email": "TEXT UNIQUE", "password": "TEXT" } } },
"routes": {
"POST /signup": { "auth": false, "pipeline": {
"hash": { "use": "hash-password", "input": "body.password" },
"user": { "use": "db.sqlite", "op": "insert", "table": "users",
"data": { "email": "{{body.email}}", "password": "{{hash}}" } },
"token": { "use": "auth.jwt", "op": "sign", "payload": { "userId": "{{user.id}}" } },
"_response": { "id": "{{user.id}}", "token": "{{token}}" }, "_status": 201
} },
"GET /me": { "use": "db.sqlite", "op": "findOne", "table": "users",
"where": { "id": "{{auth.userId}}" } }
}
}Generate a spec from OpenAPI:
apideploy from-openapi openapi.json -o mocks.json && apideploy start mocks.json🤝 The Api family
- apibooks — auto-generated documentation for Express APIs
- apideploy — build the API itself, declaratively (a submodule of apibooks)
- apiupbot — uptime monitoring bot for your APIs
📄 License
MIT
