Skip to content

HTTP APIs ​

HTTP in Cell is two packages: @celljs/web (server, middleware chain, body parsing, cookies, sessions, CORS, errors) and @celljs/mvc (controllers on top of it). Both run on Node's node:http with no third-party framework. Import the decorators from @celljs/mvc/lib/node and the request-level types from @celljs/web/lib/node.

Controllers and routes ​

ts
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@celljs/mvc/lib/node';

@Controller('users')                       // prefix: /users
export class UserController {
    @Get()                                 // GET /users?role=admin
    list(@Query('role') role?? string) { /* ... */ }

    @Get(':id')                            // GET /users/42
    get(@Param('id') id: number) { /* ... */ }

    @Post()
    create(@Body() user: CreateUser) { /* ... */ }

    @Patch(':id')
    rename(@Param('id') id: number, @Body('name') name: string) { /* ... */ }

    @Delete(':id')
    remove(@Param('id') id: number): void { /* ... */ }
}
  • Method decorators: @Get, @Post, @Put, @Patch, @Delete, @Head, @Options, @All (every method). Without a path the method answers the controller path itself.
  • Paths: :name is one segment, * matches anything, (...) is optional. server.path (default /) prefixes every route.
  • A request no route matches gets 404.
  • Controllers are components: inject services with @Autowired, read configuration with @Value. Import the controller's file from src/node/module.ts.

Parameters ​

DecoratorValue
@Param('id') / @Param()One path parameter / all of them
@Query('q') / @Query()One query parameter / the parsed query (?a[b]=1 → { a: { b: '1' } })
@Body('name') / @Body()One field / the whole body
@Header('x-api-key') / @Header()One request header / all headers
@Cookie('sid')A request cookie
@Session('cart')A value of the session
@Req() / @Res()Node's request and response (IncomingMessage / ServerResponse with extras)

Parameters typed string, number, boolean or Date are converted ('42' → 42). The body is parsed from JSON, text/plain, application/octet-stream (a Buffer) or a URL-encoded form; gzip, deflate and br bodies are decompressed. Malformed JSON answers 400, a body over web.body.limit (default 100kb) answers 413.

Validating the body ​

Add @celljs/validation and class-validator, then type the parameter with a class:

ts
import { IsString, Length } from 'class-validator';

export class CreateUser {
    @IsString()
    @Length(1, 20)
    name: string;
}

@Post()
create(@Body() user: CreateUser) { /* user is a CreateUser instance and is valid */ }

An invalid body answers 400 with the errors as JSON; the method is not called. Options are under validation (@celljs/validation).

Responses ​

Return / decoratorResponse
an object, array, number, ...JSON (application/json); the default view
@Text() and a stringtext/plain
@Html('page.mustache') and an objectthe Mustache template from mvc.mustache.baseViewDir (default assets/views) rendered with it
@File() and a Buffer or a streamthe bytes, streamed
undefined (void)200 with an empty body
@Header('cache-control', 'no-store')adds a response header
@Cookie('flavor', 'oat') / @Session('step', 'two')sets a cookie / a session value

async methods work the same way. To choose the status code, take the response: @Res() res: Response (with import type { Response } from '@celljs/web/lib/node': it is an interface) or Context.getResponse(), and set res.statusCode = 201, then return the body as usual.

Errors ​

ts
import { HttpError, NotFoundError } from '@celljs/web/lib/node';

throw new NotFoundError('No user 42');          // 404
throw new HttpError(409, 'Name already taken'); // any status
throw new Error('boom');                        // 500

The status comes from HttpError.statusCode; any other error answers 500. Stack traces are never sent to the client: a 500 is logged with its stack (and its cause) at the error level, a 4xx HttpError (or an error a handler answered with a redirect or a 4xx) only at the debug level. The client gets the message of a 4xx HttpError; in production a 5xx or any other error answers only its status text, unless the HttpError was created with { expose: true } or web.errors.exposeMessages is set. Per controller, a method marked @Catch(ErrorType) handles the errors of that type thrown in the controller, and its return value is the response:

ts
@Catch(QuotaError)
@Text()
onQuota(err: QuotaError) { return `Quota exceeded: ${err.message}`; }

For the whole application, add an ErrorHandler. Handlers are asked in order of priority (highest first) whether they canHandle the error; the first that can handles it, and if its handle throws, the next one is tried. The built-in handlers have priorities up to 2000, so give yours more to run first:

ts
import { Component } from '@celljs/core';
import { Context, ErrorHandler } from '@celljs/web/lib/node';

@Component(ErrorHandler)
export class QuotaErrorHandler implements ErrorHandler {
    readonly priority = 2100;

    async canHandle(ctx: Context, err: Error) { return err instanceof QuotaError; }

    async handle(ctx: Context, err: Error) {
        ctx.response.statusCode = 429;
        ctx.response.setHeader('content-type', 'application/json');
        ctx.response.end(JSON.stringify({ error: err.message }));
    }
}

Middleware ​

A middleware is a component bound to Middleware. It sees every request before the controllers; higher priority runs earlier.

ts
import { Component } from '@celljs/core';
import { Context, HTTP_MIDDLEWARE_PRIORITY, HttpError, Middleware } from '@celljs/web/lib/node';

@Component(Middleware)
export class ApiKeyMiddleware implements Middleware {
    readonly priority = HTTP_MIDDLEWARE_PRIORITY - 10;   // after trace, CORS and body parsing

    async handle(ctx: Context, next: () => Promise<void>): Promise<void> {
        if (ctx.request.path.startsWith('/admin') && ctx.request.get('x-api-key') !== process.env.API_KEY) {
            throw new HttpError(401, 'Missing or wrong API key');
        }
        await next();
    }
}

Middleware written for express that only needs Node's req / res can be wrapped with convertExpressMiddleware(fn).

The request context ​

Anywhere during a request, also in services far from the controller, Context from @celljs/web/lib/node gives the current request: Context.getRequest(), Context.getResponse(), Context.getSession(), Context.getCookies(), Context.getTraceId(), and request-scoped attributes with Context.setAttr(key, value) / Context.getAttr(key). It follows await, timers and promise chains.

Built in ​

FeatureConfiguration
Trace id: every response has an X-Cell-Trace-ID header (Context.getTraceId())web.trace
Health check: GET /healthweb.health
CORS headers, every origin by defaultweb.cors (origin, methods, credentials, ...)
Signed cookies and sessions stored in a cookieweb.cookies.keys, web.session. Set your own web.cookies.keys in production
Request body limitsweb.body

The @celljs/web reference has every option.

More ​

NeedPackage
OpenAPI document (/openapi.json) and Swagger UI (/api-docs) from the controllers@celljs/openapi
Login, sessions, @Authenticated(), @PreAuthorize()@celljs/security
JSON Web Tokens@celljs/jwt
Serving the frontend build@celljs/serve-static
Typed calls from the frontend or other servicesFrontend and RPC
gRPC services@celljs/grpc