Getting started
Create a project, run it, and talk to it with curl. This takes about five minutes.
Requirements#
- Node.js 22.12 or newer (24 LTS recommended). The CLI refuses to run on older versions.
- MongoDB 6 or newer, local or hosted (for example MongoDB Atlas). For a quick local server:
docker run -d --name mongo -p 27017:27017 mongo:8Create a project#
npm create calmapi@latest my-apiThe wizard asks for:
- Project folder (default
my-api). - Template:
apiorminimal(see below). - MongoDB URI (default
mongodb://127.0.0.1:27017/<project_name>), written to.env. - Whether to install dependencies and initialize git.
To skip the questions, pass flags after -- so npm forwards them:
npm create calmapi@latest my-api -- --yes --template api --db mongodb://127.0.0.1:27017/my_api| Template | What you get |
|---|---|
api (default) | A user resource with sign up, login, sessions and password reset under /api/auth, an example post resource with a service and custom routes, and a test. |
minimal | Just the framework, an app/routes.js with GET /api/hello, and a test. No user resource, so no auth until you add one. |
Every template also contains calm/ (the framework), calm.config.js, calm.lock, .env, AGENTS.md, a Dockerfile and an ESLint config. See Project structure.
Run it#
cd my-api
npm run dev CalmAPI my-api v0.1.0 (development)
➜ API http://localhost:5001/api
➜ OpenAPI http://localhost:5001/api/openapi.json
➜ Devtools http://localhost:5001/_calm
2 resources · 25 routes · 0 blocks · MongoDB connectednpm run dev runs calm dev: development mode, a restart whenever something in app/, calm/, calm.config.js, server.js or .env changes, and devtools at /_calm. If MongoDB is not reachable, the app retries a few times and then exits with a clear message.
First requests#
The examples below use the api template.
1. Register#
curl -X POST http://localhost:5001/api/auth/register \
-H 'Content-Type: application/json' \
-d '{"name":"Ada Lovelace","email":"ada@example.com","password":"correct-horse-battery"}'{
"data": {
"user": {
"id": "6acb46a57e4a738ebdd998b1",
"name": "Ada Lovelace",
"email": "ada@example.com",
"roles": ["user"],
"status": "active",
"createdAt": "2026-10-11T08:19:49.069Z",
"updatedAt": "2026-10-11T08:19:49.069Z"
},
"token": "calm_M_vAh-pIohIgNEe8gapu1LMoYxlH6oYvepNVGWtYPWk",
"expiresAt": "2026-11-10T08:19:49.085Z"
}
}Registering signs you in. The password is never returned, and roles cannot be set by the client.
2. Log in#
curl -X POST http://localhost:5001/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"ada@example.com","password":"correct-horse-battery"}'The response has the same shape. Keep the token for the next requests:
TOKEN=calm_GZWY3MMqzg4aRbB80iRCKScHbbnjrLSDu2-sfYER27E3. Create a post#
curl -X POST http://localhost:5001/api/posts \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"title":"Hello CalmAPI","body":"My first post","tags":["intro","news"],"status":"published"}'{
"data": {
"id": "6acb46a57e4a738ebdd998b4",
"title": "Hello CalmAPI",
"slug": "hello-calmapi",
"body": "My first post",
"status": "published",
"tags": ["intro", "news"],
"author": "6acb46a57e4a738ebdd998b1",
"publishedAt": "2026-10-11T08:19:49.447Z",
"createdAt": "2026-10-11T08:19:49.448Z",
"updatedAt": "2026-10-11T08:19:49.448Z"
}
}The status is 201. author was set from your session, and slug and publishedAt were filled in by post.service.js. Sending slug yourself is refused, because it is read-only:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"requestId": "5c397446-0c18-4cfe-99be-d08c78419bec",
"details": { "slug": "Is read-only", "likes": "Unknown field" }
}
}4. List with filters#
curl -g "http://localhost:5001/api/posts?status=published&tags[in]=intro,news&sort=-createdAt&include=author&fields=title,author" \
-H "Authorization: Bearer $TOKEN"-g stops curl from reading [in] as a glob pattern.
{
"data": [
{
"id": "6acb46a57e4a738ebdd998b4",
"title": "Hello CalmAPI",
"author": {
"id": "6acb46a57e4a738ebdd998b1",
"name": "Ada Lovelace",
"email": "ada@example.com",
"roles": ["user"],
"status": "active",
"createdAt": "2026-10-11T08:19:49.069Z",
"updatedAt": "2026-10-11T08:19:49.069Z"
},
"createdAt": "2026-10-11T08:19:49.448Z",
"updatedAt": "2026-10-11T08:19:49.448Z"
}
],
"meta": { "total": 1, "page": 1, "limit": 20, "pages": 1 }
}You see the author's email and roles because the author is you. Anyone else gets the author without them: the user resource marks them .visibleTo('owner | admin'). Without a token, the list only contains published posts, because post.service.js scopes every query.
Ask for something that is not allowed and you get a 400 that explains why:
curl -g "http://localhost:5001/api/posts?body=x&sort=body&limit=500"{
"error": {
"code": "BAD_REQUEST",
"message": "Invalid query",
"requestId": "86f489d3-d343-4d90-a614-86bdca30b700",
"details": {
"body": "Not filterable. Filterable fields: slug, status, tags",
"sort": "Cannot sort by \"body\". Sortable: id, title, publishedAt, createdAt, updatedAt",
"limit": "Must be an integer between 1 and 100"
}
}
}The full query language is in Querying.
Open the devtools#
Visit http://localhost:5001/_calm. Every request you just made is there with its MongoDB queries, timings and logs. Ask for a password reset (POST /api/auth/password/forgot) and the email shows up in the Mail panel, link included.
Add your own resource#
npx calm make resource product name:string!+searchable price:number! status:enum:draft,live=draft --service✔ Created resource product at /api/products
+ app/resources/product/product.resource.js
+ app/resources/product/product.service.js
+ test/products.test.jsThe dev server restarts and /api/products is live with all five CRUD routes. Open the resource file to adjust fields and policies, then:
npx calm routes # every route with its policy
npx calm doctor # security and correctness checks
npx calm audit # every route as every role, on a throwaway database
npm test # node --test, needs MongoDBNext steps#
- Project structure: what each file and folder is for.
- Resources and fields: every field type and modifier.
- Policies: who may list, read, create, update and delete.