Getting started
This page takes you from an empty directory to a tested, built and deployable Cell backend. It assumes Node.js ^20.19.0 || >=22.12.0 for development; the built application runs on Node.js 18 or later, Bun 1.2 or later and Deno 2.
Create a project
npx @celljs/cli init backend my-app # or: fullstack (React frontend + backend over RPC)
cd my-app
npm install
npx cell dev # http://127.0.0.1:3000, reloads on savecell init never asks questions. Without a template name it lists the templates; a non-empty directory needs --force. The project it writes:
my-app/
├── cell.yml # application configuration, read with @Value
├── package.json # scripts: dev, build, preview, check, test
├── tsconfig.json # experimentalDecorators + emitDecoratorMetadata
├── src/node/
│ ├── module.ts # the backend DI module: imports every file with components
│ └── home-controller.ts
├── test/app.spec.ts # bun test + createTestApp()
├── AGENTS.md, CLAUDE.md # guide for coding agents, matched to the installed version
├── .agents/skills/cell/ # the same guide as an agent skill
└── .mcp.json # `npx cell mcp` for agents that speak MCPAdd an endpoint
Create a controller, then import its file from src/node/module.ts. A file that no module imports is not loaded, so its components do not exist.
// src/node/todo-controller.ts
import { Body, Controller, Delete, Get, Param, Post } from '@celljs/mvc/lib/node';
import { NotFoundError } from '@celljs/web/lib/node';
interface Todo { id: number; title: string }
@Controller('todos')
export class TodoController {
protected readonly todos = new Map<number, Todo>();
protected nextId = 1;
@Get()
list(): Todo[] {
return [...this.todos.values()];
}
@Get(':id')
get(@Param('id') id: number): Todo {
const todo = this.todos.get(id);
if (!todo) {
throw new NotFoundError(`No todo ${id}`); // 404
}
return todo; // objects are sent as JSON
}
@Post()
create(@Body('title') title: string): Todo {
const todo = { id: this.nextId++, title };
this.todos.set(todo.id, todo);
return todo;
}
@Delete(':id')
remove(@Param('id') id: number): void {
this.todos.delete(id);
}
}// src/node/module.ts
import './home-controller';
import './todo-controller';
import { autoBind } from '@celljs/core';
export default autoBind();@celljs/web is a dependency of @celljs/mvc; add it to package.json too when you import from it (npm install @celljs/web).
curl -X POST localhost:3000/todos -H 'content-type: application/json' -d '{"title":"write docs"}'
curl localhost:3000/todos/1Move the state into a service when it grows: see Components and dependency injection. The controller decorators are listed in HTTP APIs.
Configure it
cell.yml holds the application configuration: keys of the components (server.port, ...) at the top level, your own under app. Configuration is server-only; only app.public and keys components mark public reach the browser:
app:
greeting: Hello from my-app
todos:
max: ${(env.TODOS_MAX ?? 100) * 1} # evaluated when the server starts; keys under app have no schema type, * 1 makes the env text a number@Value('app.todos.max')
protected readonly max: number;npx cell config get app.todos.max --why shows the merged value and every file or variable that set it. More in Project structure and configuration.
Check and test
npx cell check # TypeScript, configuration, decorator settings, DI bindings
bun test # the tests under test/// test/todo.spec.ts
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
import { join } from 'node:path';
import { createTestApp, type TestApp } from '@celljs/cli';
describe('todos', () => {
let app: TestApp;
beforeAll(async () => { app = await createTestApp({ root: join(__dirname, '..') }); }, 60000);
afterAll(() => app?.close());
it('creates a todo', async () => {
const res = await app.fetch('/todos', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{"title":"a"}' });
expect(await res.json()).toEqual({ id: 1, title: 'a' });
});
});createTestApp() starts the whole application inside the test process without opening a port. See Testing.
Build and run
npx cell build # .cell/output/server/index.mjs: one file with every dependency inlined
npx cell preview # runs the output as production would
node .cell/output/server/index.mjs--single puts the frontend into the same file, --compile makes an executable that needs no Node.js on the host.
Deploy
npx cell deploy --preset cloudflare --dry-run # the plan, nothing is uploaded
npx cell deploy --preset cloudflareThe presets are node (default), cli, cloudflare, fc, scf, lambda and vercel. See Building and deploying.
Next
- Package catalog: which
@celljs/*package to add for databases, caching, scheduled jobs, security, AI and more. - Working with coding agents: the AGENTS.md, skill and MCP tools that
cell initset up.