Documentation chapters
Framework

HTTP controller

An HTTP controller is one of three different types of entry points to your application. An HTTP controller defines HTTP routes using TypeScript classes and decorators.

A very simple implementation of an HTTP controller looks like the following:

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http } from '@deepkit/http';

class MyPage {
    @http.GET('/')
    helloWorld() {
        return "Hello World!";
    }
}

new App({
    controllers: [MyPage],
    imports: [new FrameworkModule]
}).run();

You can execute that script directly with ts-node.

$ ts-node app.ts server:start
2021-06-11T17:44:52.646Z [LOG] Start HTTP server, using 1 workers.
2021-06-11T17:44:52.649Z [LOG] HTTP MyPage
2021-06-11T17:44:52.649Z [LOG]     GET / helloWorld
2021-06-11T17:44:52.649Z [LOG] HTTP listening at http://localhost:8080/

Request your first route with curl to see your returned string.

$ curl http://localhost:8080/
Hello World!

HTTP controllers are handled and instantiated by the dependency injection container, like services and event listeners, and thus have access to all other registered services. See the chapter Dependency injection for more details.

Decorators

Each class needs at least the @http.controller decorator and each route at least one HTTP method decorator. Decorators on routes (methods) may be chained, for example:

@http.GET('/user').name('users').description('Lists users')
userList() {
}
Decorator/TypeDescription
@http.controller(string)Marks a class as controller with a baseUrl.
@http.name(string)Assigns a unique name to this route.
@http.GET(string)Marks a method as HTTP GET route with a path pattern.
@http.POST(string)Marks a method as HTTP POST route with a path pattern.
@http.PUT(string)Marks a method as HTTP PUT route with a path pattern.
@http.DELETE(string)Marks a method as HTTP DELETE route with a path pattern.
@http.ANY(string)Marks a method as any HTTP method route with a path pattern.
@http.throws(ClassType, string)Annotates possible error classes that can be thrown by this class.
@http.regexp(string, RegExp)Specifies the regular expression for a route parameter.
@http.group(...string)Assigns this route to a specific group/s.
@http.category(string)Assigns this route to a category.
@http.data(string, any)Assigns arbitrary data to this route.
@http.serialization()Assigns serialization options for the serialization process of @deepkit/type types.
@http.serializer()Assigns a different serializer for the serialization process of @deepkit/type types. Default is serializer
@http.description(string)Assigns a description to this route that will be used in developer tooling
@http.use(Function)Allows to change the route object and composite multiple properties into one function.

Types

HttpQueryMarks a route parameter as query parameter (so it is read from the query string).
HttpQueriesMarks a route parameter as a type of all query parameters (parsed query string as an object).
HttpBodyMarks a route parameter as body object (so the parsed body is passed)
HttpBodyValidationMarks a route parameter as body object (so the parsed body is passed) with custom validation handling.

A Note on Decorator Metadata

One of the powerful use cases for decorators is the ability to attach data to parts of your actual application code. This data can then be retrieved and further built upon later. By providing descriptive metadata to your controllers we can enable more powerful tooling like (eventual) OpenAPI support as well as deep integration with the Deepkit API Console GUI

Parameters

Routes can have arbitrary parameters. Path parameters are expressed in the route's path and query as well as body parameters in the method signature itself. Path parameter's name maps directly to method arguments with the same name.

Path parameters

class MyPage {
    @http.GET('/:text')
    helloWorld(text: string) {
        return 'Hello ' + text;
    }
}
$ curl http://localhost:8080/galaxy
Hello galaxy

You can modify how a path parameter is matched using @http.regexp(name, regex).

@http.GET('hello-world/:text').regexp('text', /a-zA-Z0-9/)
helloWorld(text: string) {
    return 'Hello ' + text;
}

Query parameters

import { HttpQuery } from '@deepkit/http';

class MyPage {
    @http.GET('/')
    helloWorld(text: HttpQuery<string>) {
        return 'Hello ' + text;
    }
}
$ curl http://localhost:8080/\?text\=galaxy
Hello galaxy

Parameter types are automatically deserialized and validated. So you can add validators of @deepkit/type at your parameters.

import { HttpQuery, MinLength } from '@deepkit/http';

class MyPage {
    @http.GET('/')
    helloWorld(text: HttpQuery<string> & MinLength<3>) {
        return 'Hello ' + text;
    }
}
$ curl http://localhost:8080/\?text\=galaxy
Hello galaxy
$ curl http://localhost:8080/\?text\=ga
error

Warning: Parameter values are not escaped/sanitized. Returning them directly in a string opens a security hole (XSS). Make sure never to trust external input and filter/sanitize/convert where necessary.

Query parameters model

Instead of specifying each query parameter as a method parameter, you can use a type instead.

class HelloWorldQuery {
    text!: string;
    page: number = 0;
}

class MyPage {
    @http.GET('/')
    helloWorld(query: HttpQueries<HelloWorldQuery>) {
        return 'Hello ' + query.text + ' at page ' + query.page;
    }
}

Body parameters

For @http.POST routes, you can specify a body schema that is automatically deserialized from the incoming HTTP body. The body content type needs to be either application/x-www-form-urlencoded, multipart/form-data, or application/json.

class HelloWorldBody {
    text!: string;
}

class MyPage {
    @http.POST('/')
    helloWorld(body: HttpBody<HelloWorldBody>) {
        return 'Hello ' + body.text;
    }
}
$ curl http://localhost:8080/ -H "Content-Type: application/json" --data '{"text": "galaxy"}'
Hello galaxy

To react to body validation errors in the same route, you use HttpBodyValidation.

@http.POST('hello-world')
helloWorld(body: HttpBodyValidation<HelloWorldBody>) {
    if (!body.valid()) {
        // Houston, we got some errors.
        const textError = body.getErrorMessageForPath('text');
        return 'Text is invalid, please fix it. ' + textError;
    }
        
    return 'Hello ' + body.text;
}

As soon as valid() returns false, the injected body representation body can be in a faulty state since the validation failed. When you don't use HttpBodyValidation and a faulty request comes in, the whole route would return an error and your route code would never execute. Use HttpBodyValidation only when you want to for example display error messages regarding the body manually in the same route.

To accept uploaded files from HTML forms for example, you should use UploadedFile.

import { http, UploadedFile, HttpBody } from '@deepkit/http';
import { readFileSync } from 'fs';

class HelloWordBody {
    file!: UploadedFile;
}

class MyPage {
    @http.POST('/')
    helloWorld(body: HttpBody<HelloWordBody>) {
        const content = readFileSync(body.file.path);

        return {
            uploadedFile: body.file
        };
    }
}
$ curl http://localhost:8080/ -X POST -H "Content-Type: multipart/form-data" -F "file=@Downloads/23931.png"
{
    "uploadedFile": {
        "size":6430,
        "path":"/var/folders/pn/40jxd3dj0fg957gqv_nhz5dw0000gn/T/upload_dd0c7241133326bf6afddc233e34affa",
        "name":"23931.png",
        "type":"image/png",
        "lastModifiedDate":"2021-06-11T19:19:14.775Z"
    }
}

By default, the router stores all uploaded files in the temp folder and will be removed as soon as your route method is finished.

Parameter validation

Parameters are automatically converted to the annotated type and validated. See Deepkit Types chapter for more information.

import { Positive, Max } from '@deepkit/type';

class MyWebsite {
    @http.GET(':id')
    getUser(id: number & Positive & Max<10000>) {
        //...
    }
}

Parameter resolver

The router supports a way to resolve complex parameter types. If you have for example a route like /user/:id you can resolve the id into a User object outside of the route using a resolver.

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http, RouteParameterResolverContext, RouteParameterResolverTag, RouteParameterResolver } from '@deepkit/http';
import { ClassType } from '@deepkit/core';

class User {
    constructor(
        public username: string,
        public id: number = 0,
    ) {
    }
}

class UserDatabase {
    protected users: User[] = [
        new User('User 1', 1),
        new User('User 2', 2),
    ];

    public getUser(id: number): User | undefined {
        return this.users.find(v => v.id === id);
    }
}

class UserResolver implements RouteParameterResolver {
    constructor(protected database: UserDatabase) {
    }

    async resolve(context: RouteParameterResolverContext) {
        if (!context.parameters.id) throw new Error('No :id given');
        return await this.database.getUser(parseInt(context.parameters.id));
    }
}

@http.resolveParameter(User, UserResolver)
class MyWebsite {
    @http.GET(':id')
    getUser(user: User) {
        return 'Hello ' + user.username;
    }
}

new App({
    controllers: [MyWebsite],
    providers: [
        UserDatabase,
        UserResolver,
    ],
    imports: [new FrameworkModule]
})
    .run();

Response

A controller can return various data structures. Some of them are treated in a special way like redirects and templates, and others like simple objects are simply sent as JSON.

Types

If they route has a return type defined the returning value is automatically serialized using JSON serializer and sent using content type application/json; charset=utf-8.

You can additionally define how the result is serialized using serialization and serializer.

import { Group } from '@deepkit/type';

class User {
    passwordHash?: string & Group<'sensitive'>;

    constructor(
        public username: string,
        public id: number = 0,
    ) {
    }
}

class MyWebsite {
    @http.GET('/user').serialization({groupsExclude: ['sensitive']})
    getUsers(): User[] {
        return this.users.list;
    }
}

Redirect

Redirecting a user may be done via the Redirect object. By default it uses 302 redirects, but that can be changed in the arguments of Redirect.toRoute and Redirect.toUrl.

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http, Redirect, HttpBody } from '@deepkit/http';

class User {
    constructor(
        public username: string,
        public id: number = 0,
    ) {}
}

class Users {
    public list: User[] = [];
}

class MyWebsite {
    constructor(protected users: Users) {
    }

    @http.GET('/user').name('user_list')
    getUsers() {
        return this.users.list;
    }

    @http.POST('/user')
    addUser(user: HttpBody<User>) {
        this.users.list.push(user);
        return Redirect.toRoute('user_list');
    }
}

new App({
    providers: [Users],
    controllers: [MyWebsite],
    imports: [new FrameworkModule]
}).run();

Modification

A response from a controller can be arbitrarily changed and reacted on using an event listener.

To change the result of a route call, listen to the httpWorkflow.onResponse event and change the event.result accordingly.

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http, httpWorkflow } from '@deepkit/http';
import { eventDispatcher } from '@deepkit/event';

class User {
    constructor(
        public username: string,
        public id: number = 0,
    ) {
    }
}

class MyWebsite {
    @http.GET()
    getUser() {
        return new User('User 1', 1);
    }
}

class UserResponseMapping {
    @eventDispatcher.listen(httpWorkflow.onResponse)
    onResponse(event: typeof httpWorkflow.onResponse.event) {
        if (event.result instanceof User) {
            event.result = event.result.username;
        }
    }
}

new App({
    controllers: [MyWebsite],
    listeners: [UserResponseMapping],
    imports: [new FrameworkModule]
})
    .run();
Made in Germany