Skip to content

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 ​

bash
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 save

cell init never asks questions. Without a template name it lists the templates; a non-empty directory needs --force. The project it writes:

text
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 MCP

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

ts
// 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);
    }
}
ts
// 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).

bash
curl -X POST localhost:3000/todos -H 'content-type: application/json' -d '{"title":"write docs"}'
curl localhost:3000/todos/1

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

yaml
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
ts
@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 ​

bash
npx cell check        # TypeScript, configuration, decorator settings, DI bindings
bun test              # the tests under test/
ts
// 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 ​

bash
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 ​

bash
npx cell deploy --preset cloudflare --dry-run   # the plan, nothing is uploaded
npx cell deploy --preset cloudflare

The presets are node (default), cli, cloudflare, fc, scf, lambda and vercel. See Building and deploying.

Next ​