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:
Shell
docker run -d --name mongo -p 27017:27017 mongo:8

Create a project#

Shell
npm create calmapi@latest my-api

The wizard asks for:

  1. Project folder (default my-api).
  2. Template: api or minimal (see below).
  3. MongoDB URI (default mongodb://127.0.0.1:27017/<project_name>), written to .env.
  4. Whether to install dependencies and initialize git.

To skip the questions, pass flags after -- so npm forwards them:

Shell
npm create calmapi@latest my-api -- --yes --template api --db mongodb://127.0.0.1:27017/my_api
TemplateWhat 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.
minimalJust 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#

Shell
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 connected

npm 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#

Shell
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"}'
JSON
{
  "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#

Shell
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:

Shell
TOKEN=calm_GZWY3MMqzg4aRbB80iRCKScHbbnjrLSDu2-sfYER27E

3. Create a post#

Shell
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"}'
JSON
{
  "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:

JSON
{
  "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#

Shell
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.

JSON
{
  "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:

Shell
curl -g "http://localhost:5001/api/posts?body=x&sort=body&limit=500"
JSON
{
  "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#

Shell
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.js

The dev server restarts and /api/products is live with all five CRUD routes. Open the resource file to adjust fields and policies, then:

Shell
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 MongoDB

Next steps#