Project structure

A CalmAPI project is a plain Node.js (ESM) app. The framework is in calm/, your code is in app/.

my-api/
├── calm/                     the framework (yours, tracked by calm.lock)
├── app/
│   ├── resources/
│   │   ├── post/
│   │   │   ├── post.resource.js    fields + access policy (required)
│   │   │   ├── post.service.js     business logic (optional)
│   │   │   └── post.routes.js      custom endpoints (optional)
│   │   └── user/
│   │       └── user.resource.js
│   ├── blocks/               blocks added with `calm add` (and your own plugins)
│   └── routes.js             app-wide custom routes (optional)
├── test/
│   └── posts.test.js         node:test, uses createTestApp
├── server.js                 starts the app
├── calm.config.js            config overrides (no secrets)
├── calm.lock                 framework and block file hashes, used by `calm upgrade`
├── .env / .env.example       secrets and environment
├── .calm/                    activity log written by calm dev and calm mcp (git-ignored)
├── AGENTS.md / CLAUDE.md     instructions for AI coding agents
├── Dockerfile / .dockerignore
├── eslint.config.js
└── package.json

calm/#

The framework itself, copied from the calmapi package when the project was created. It is your code: read it, debug it, change it. calm upgrade merges upstream fixes into it without discarding your edits (see Upgrading).

FileWhat it does
index.jsEverything #calm exports
app.jscreateApp(): loads config, resources and blocks, builds the Express app, starts and stops it
config.jsDefaults, .env and calm.config.js loading, validation at boot
loader.jsFinds app/resources/*, app/blocks/* and app/routes.js; definePlugin()
resource/resource.jsresource(), Mongoose compilation, input validation, output serialization
resource/fields.jsThe f.*() field builders and value validation
resource/policy.jsPolicy parsing and evaluation, hasRole(), isOwner()
resource/query.jsThe list query language (filters, search, sort, pagination, include, fields)
resource/routes.jsThe five CRUD routes
resource/service.jsResourceService
http/router.jsThe route builder, request context (ctx), item authorization, responses
http/middleware.jsRequest ids, security headers, CORS, rate limiting, JSON guard, access log
auth/Register, login, sessions, password change and reset, lockout, password rules; scrypt hashing; tokens
security/Field encryption (encryption.js), the exposure matrix (exposure.js) and the calm audit engine (audit.js)
errors.jsCalmError, helpers and the mapping of every error to a safe response
events.js, mail.js, logger.js, context.jsEvent bus, mailer, structured logger, per-request context
openapi.js, doctor.js, testing.jsOpenAPI 3.1 generator, runtime checks, test helpers
devtools/index.jsThe devtools collector and API behind /_calm (development only)

app/resources#

One folder per resource. The folder name is the resource name, and files are named after the folder:

  • <name>.resource.js (required) default-exports resource('<name>', { fields, policy }). See Resources and fields.
  • <name>.service.js (optional) default-exports a class that extends ResourceService. See Services and hooks.
  • <name>.routes.js (optional) default-exports (r) => { ... }. Its routes are mounted under the resource path, before the CRUD routes. See Custom routes.

A folder without a .resource.js file is ignored. Create resources with npx calm make resource, which also writes a test.

app/blocks#

Blocks added with calm add land here as app/blocks/<name>/index.js (some also add resources, app/jobs/ or app/notifications/). Every folder with an index.js that default-exports definePlugin({ name, setup }) is loaded at boot, so this is also the place for your own blocks, for example an event listener. npx calm make block <name> starts one. See Building your own block for the plugin API.

app/routes.js#

Routes that do not belong to a resource, mounted under /api:

JavaScript
// app/routes.js
export default (r) => {
  r.get('/hello', { policy: 'public', summary: 'Say hello' }, (ctx) => ({
    message: `Hello from ${ctx.config.app.name}`,
  }));
};

server.js#

JavaScript
import { createApp } from '#calm';

try {
  const app = await createApp({ root: import.meta.dirname });
  await app.listen();
} catch (err) {
  console.error(err.code === 'CALM_CONFIG' ? `\n${err.message}\n` : err);
  process.exit(1);
}

npm start runs it (production mode unless NODE_ENV says otherwise), npm run dev runs it through calm dev.

calm.config.js and .env#

calm.config.js overrides the defaults in calm/config.js. Secrets (database URI, API keys) belong in .env, which is git-ignored. See Configuration.

JavaScript
import { defineConfig } from '#calm';

export default defineConfig({
  pagination: { defaultLimit: 20, maxLimit: 100 },
  auth: { sessionTtlDays: 30, resetTokenTtlMinutes: 30 },
  mail: { from: 'My API <no-reply@example.com>' },
});

calm.lock#

Written by calm create and updated by calm add and calm upgrade. Commit it.

JSON
{
  "lockfileVersion": 1,
  "calmapi": "2.0.0-alpha.0",
  "template": "api",
  "core": {
    "calm/app.js": "sha256:585bd812cfc3d1343684effb382612d0074b82c7da27ede3673bb4f76df5e4a2",
    "calm/auth/index.js": "sha256:b4633aa522492d2f0065ba052660ca6f12fd813d7c90a824c7f0375ba2b130d1"
  },
  "blocks": {
    "@calmapi/jobs": {
      "version": "1.0.0",
      "source": "@calmapi (bundled)",
      "files": { "app/blocks/jobs/index.js": "sha256:..." }
    }
  }
}

core holds the hash of each framework file as it was shipped, which is how calm upgrade knows whether you edited a file and calm doctor reports local changes. blocks records what calm add installed.

AGENTS.md and CLAUDE.md#

AGENTS.md tells AI coding agents (Claude Code, Codex, Cursor, Copilot) how this project is laid out and which rules to follow. CLAUDE.md points Claude Code to it. See AI agents.

#calm and #app imports#

package.json declares subpath imports, so you never write ../../../calm:

JSON
{
  "imports": {
    "#calm": "./calm/index.js",
    "#calm/*": "./calm/*",
    "#app/*": "./app/*"
  }
}
JavaScript
import { resource, f, ResourceService, notFound } from '#calm';     // the public API
import { createTestApp } from '#calm/testing.js';                    // any framework file
import { seed } from '#calm/seed.js';
import { defineJob } from '#app/blocks/jobs/index.js';               // your own code

Subpath imports need the .js extension. Inside services and routes, reach other resources through this.service('name') or ctx.services.name instead of importing their models.

What #calm exports#

ExportUse
resource, f, FieldDeclare resources and fields
ResourceServiceBase class for services
definePluginDeclare a plugin (what blocks export)
createApp, defineConfigBuild the app, type-friendly config
CalmError, badRequest, unauthorized, forbidden, notFound, conflict, validationError, tooManyRequestsErrors (see Errors)
hasRole, isOwner, evaluatePolicy, parsePolicyPolicy helpers
matchesFilterWhether a record matches a MongoDB filter, in memory
serialize, validateInput, standardSchema, resourceJSONSchemaApply a resource's output and input rules yourself, or describe it as JSON Schema
ReplyWhat ctx.reply() returns
createToken, hashTokenRandom tokens with a prefix, and their SHA-256 for storage
hashPassword, verifyPasswordscrypt helpers
isEncryptedWhether a stored value is .encrypted() ciphertext
maskToken, providerPayloadsPush helpers: a masked device token, the payload each provider receives
currentRequestThe current request's context from anywhere (AsyncLocalStorage)

Blocks import only from #calm: the other files in calm/ are internals that can change between versions.

How the app boots#

createApp() does the following, in order:

  1. Load .env, calm.config.js and the defaults; fail fast on invalid config.
  2. Load app/resources/* and app/blocks/*, then run each block's setup() with its settings (blocks.<name> in calm.config.js merged over the block's defaults).
  3. Compile every resource into a Mongoose model and create its service.
  4. Set up auth, then register routes: auth, block routes, app/routes.js, then for each resource its custom routes followed by its CRUD routes.
  5. Build the Express app. listen() then connects to MongoDB, builds indexes, runs start hooks and prints the banner.