Framework

Installation

Deepkit Framework is based on runtime types in Deepkit Type. Make sure that @deepkit/type is installed correctly. See Runtime Type Installation.

npm install ts-node @deepkit/framework

Make sure that all peer dependencies are installed. By default, NPM 7+ installs them automatically.

To compile your application, we need the TypeScript compiler and recommend ts-node to easily run the app.

An alternative to using ts-node is to compile the source code with the TypeScript compiler and execute the JavaScript source code directly. This has the advantage of dramatically increasing execution speed for short commands. However, it also creates additional workflow overhead by either manually running the compiler or setting up a watcher. For this reason, ts-node is used in all examples in this documentation.

First Application

Since the Deepkit framework does not use configuration files or a special folder structure, you can structure your project however you want. The only two files you need to get started are the TypeScript app.ts file and the TypeScript configuration tsconfig.json.

Our goal is to have the following files in our project folder:

.
├── app.ts
├── node_modules
├── package-lock.json
└── tsconfig.json

file: tsconfig.json

{
  "compilerOptions": {
    "outDir": "./dist",
    "experimentalDecorators": true,
    "strict": true,
    "esModuleInterop": true,
    "target": "ES2020",
    "module": "CommonJS",
    "moduleResolution": "node"
  },
  "reflection": true,
  "files": [
    "app.ts"
  ]
}

File: app.ts

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { Logger } from '@deepkit/logger';
import { cli, Command } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

@cli.controller('test')
export class TestCommand implements Command {
    constructor(protected logger: Logger) {
    }

    async execute() {
        this.logger.log('Hello World!');
    }
}

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

In this code, you can see that we have defined a test command using the TestCommand class and created a new application that we run directly using run(). By running this script, we start the app.

With the shebang in the first line (#!…​) we can make our script executable with the following command.

chmod +x app.ts

And then execute:

$ ./app.ts
VERSION
  Node

USAGE
  $ ts-node-script app.ts [COMMAND]

TOPICS
  debug
  migration  Executes pending migration files. Use migration:pending to see which are pending.
  server     Starts the HTTP server

COMMANDS
  test

Now, to execute our test command, we run the following command.

$ ./app.ts test
Hello World

In Deepkit Framework everything is now done via this app.ts. You can rename the file as you like or create more. Custom CLI commands, HTTP/RPC server, migration commands, etc are all started from this entry point.

To start the HTTP/RPC server, do the following:

./app.ts server:start

To serve requests please read chapter HTTP or RPC. In chapter CLI you can learn more about CLI commands.

App

Via the App object starts like application.

The run() method lists the arguments and executes the corresponding CLI controller. Since FrameworkModule provides its own CLI controllers, which are responsible for starting the HTTP server, for example, these can be called via it.

The App object can also be used to access the Dependency Injection container without running a CLI controller.

const app = new App({
    controllers: [TestCommand],
    imports: [new FrameworkModule]
});

//get access to all registered services
const eventDispatcher = app.get(EventDispatcher);

//then run the app, or do something else
app.run();

Modules

Deepkit framework is highly modular and allows you to split your application into several handy modules. Each module has its own dependency injection sub-container, configuration, commands and much more. In the chapter "First application" you have already created one module - the root module. new App takes almost the same arguments as a module, because it creates the root module for you automatically in the background.

You can skip this chapter if you do not plan to split your application into submodules, or if you do not plan to make a module available as a package to others.

A module is a simple class:

import { createModule } from '@deepkit/app';

export class MyModule extends createModule({}) {
}

It basically has no functionality at this point because its module definition is an empty object and it has no methods, but this demonstrates the relationship between modules and your application (your root module). This MyModule module can then be imported into your application or into other modules.

import { MyModule } from './module.ts'

new App({
    imports: [
        new MyModule(),
    ]
}).run();

You can now add features to this module as you would with App. The arguments are the same, except that imports are not available in a module definition. Add HTTP/RPC/CLI controllers, services, a configuration, event listeners, and various module hooks to make modules more dynamic.

Controllers

Modules can define controllers that are processed by other modules. For example, if you add a controller with decorators from the @deepkit/http package, its HttpModule module will pick this up and register the found routes in its router. A single controller may contain several such decorators. It is up to the module author who gives you these decorators how he processes the controllers.

In Deepkit there are three packages that handles such controllers: HTTP, RPC, and CLI. See their respective chapters to learn more. Below is an example of an HTTP controller:

import { createModule } from '@deepkit/app';
import { http } from '@deepkit/http';
import { injectable } from '@deepkit/injector';

class MyHttpController {
    @http.GET('/hello)
    hello() {
        return 'Hello world!';
    }
}

export class MyModule extends createModule({
    controllers: [MyHttpController]
}) {}

//same is possible for App
new App({
    controllers: [MyHttpController]
}).run();

Provider

When you define a provider in the providers section of your application, it is accessible throughout your application. For modules, however, these providers are automatically encapsulated in that module’s dependency injection subcontainer. You must manually export each provider to make it available to another module or your application.

To learn more about how providers work, please refer to the Dependency Injection chapter.

import { createModule } from '@deepkit/app';
import { http } from '@deepkit/http';
import { injectable } from '@deepkit/injector';

export class HelloWorldService {
    helloWorld() {
        return 'Hello there!';
    }
}

class MyHttpController {
    constructor(private helloService: HelloWorldService) {}

    @http.GET('/hello)
    hello() {
        return this.helloService.helloWorld();
    }
}

export class MyModule extends createModule({
    controllers: [MyHttpController],
    providers: [HelloWorldService],
}) {}

//same is possible for App
new App({
    controllers: [MyHttpController],
    providers: [HelloWorldService],
}).run();

When a user imports this module, he has no access to HelloWorldService because it is encapsulated in the subdependency injection container of MyModule.

Exports

To make providers available in the importer’s module, you can include the provider’s token in exports. This essentially moves the provider up one level into the dependency injection container of the parent module - the importer.

import { createModule } from '@deepkit/app';

export class MyModule extends createModule({
    controllers: [MyHttpController]
    providers: [HelloWorldService],
    exports: [HelloWorldService],
}) {}

If you have other providers like FactoryProvider, UseClassProvider etc., you should still use only the class type in the exports.

import { createModule } from '@deepkit/app';

export class MyModule extends createModule({
    controllers: [MyHttpController]
    providers: [
        {provide: HelloWorldService, useValue: new HelloWorldService}
    ],
    exports: [HelloWorldService],
}) {}

We can now import that module and use its exported service in our application code.

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { cli, Command } from '@deepkit/app';
import { HelloWorldService, MyModule } from './my-module';

@cli.controller('test')
export class TestCommand implements Command {
    constructor(protected helloWorld: HelloWorldService) {
    }

    async execute() {
        this.helloWorld.helloWorld();
    }
}

new App({
    controllers: [TestCommand],
    imports: [
        new MyModule(),
    ]
}).run();

Read the Dependency Injection chapter to learn more.

Configuration

In Deepkit framework, modules and your application can have configuration options. For example, a configuration can consist of database URLs, passwords, IPs, and so on. Services, HTTP/RPC/CLI controllers, and template functions can read these configuration options via dependency injection.

A configuration can be defined by defining a class with properties. This is a type-safe way to define a configuration for your entire application, and its values are automatically serialized and validated.

Example

import { MinLength } from '@deepkit/type';
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http } from '@deepkit/http';

class Config {
    pageTitle: string & MinLength<2> = 'Cool site';
    domain: string = 'example.com';
    debug: boolean = false;
}

class MyWebsite {
    constructor(protected allSettings: Config) {
    }

    @http.GET()
    helloWorld() {
        return 'Hello from ' + this.allSettings.pageTitle + ' via ' + this.allSettings.domain;
    }
}

new App({
    config: Config,
    controllers: [MyWebsite],
    imports: [new FrameworkModule]
}).run();
$ curl http://localhost:8080/
Hello from Cool site via example.com

Configuration Class

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

export class Config {
    title!: string & MinLength<2>; //this makes it required and needs to be provided
    host?: string;

    debug: boolean = false; //default values are supported as well
}
import { createModule } from '@deepkit/app';
import { Config } from './module.config.ts';

export class MyModule extends createModule({
   config: Config
}) {}

The values for the configuration options can be provided either in the constructor of the module, with the .configure() method or via configuration loaders (e.g. environment variable loaders).

import { MyModule } from './module.ts';

new App({
   imports: [new MyModule({title: 'Hello World'}],
}).run();

To dynamically change the configuration options of an imported module, you can use the process hook. This is a good place to either redirect configuration options or set up an imported module depending on the current module configuration or other module instance information.

import { MyModule } from './module.ts';

export class MainModule extends createModule({
}) {
    process() {
        this.getImportedModuleByClass(MyModule).configure({title: 'Changed'});
    }
}

At the application level, it works a little differently:

new App({
    imports: [new MyModule({title: 'Hello World'}],
})
    .setup((module, config) => {
        module.getImportedModuleByClass(MyModule).configure({title: 'Changed'});
    })
    .run();

When the root application module is created from a regular module, it works similarly to regular modules.

class AppModule extends createModule({
}) {
    process() {
        this.getImportedModuleByClass(MyModule).configure({title: 'Changed'});
    }
}

App.fromModule(new AppModule()).run();

Configuration Options Readout

To use a configuration option in a service, you can use normal dependency injection. It is possible to inject either the entire configuration object, a single value, or a portion of the configuration.

Partial

To inject only a subset of the configuration values, use the pick type.

import { Config } from './module.config';

export class MyService {
     constructor(private config: Pick<Config, 'title' | 'host'}) {
     }

     getTitle() {
         return this.config.title;
     }
}


//In unit tests, it can be instantiated via
new MyService({title: 'Hello', host: '0.0.0.0'});

//or you can use type aliases
type MyServiceConfig = Pick<Config, 'title' | 'host'};
export class MyService {
     constructor(private config: MyServiceConfig) {
     }
}

Single Value

To inject only a single value, use the index access operator.

import { Config } from './module.config';

export class MyService {
     constructor(private title: Config['title']) {
     }

     getTitle() {
         return this.title;
     }
}

All

To inject all config values, use the class as dependency.

import { Config } from './module.config';

export class MyService {
     constructor(private config: Config) {
     }

     getTitle() {
         return this.config.title;
     }
}

Debugger

The configuration values of your application and all modules can be displayed in the debugger. Activate the debug option in the FrameworkModule and open http://localhost:8080/_debug/configuration.

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

new App({
    config: Config,
    controllers: [MyWebsite],
    imports: [
        new FrameworkModule({
            debug: true,
        })
    ]
}).run();
debugger configuration

You can also use ts-node app.ts app:config to display all available configuration options, the active value, their default value, description and data type.

$ ts-node app.ts app:config
Application config
┌─────────┬───────────────┬────────────────────────┬────────────────────────┬─────────────┬───────────┐
│ (index) │     name      │         value          │      defaultValue      │ description │   type    │
├─────────┼───────────────┼────────────────────────┼────────────────────────┼─────────────┼───────────┤
│    0    │  'pageTitle'  │     'Other title'      │      'Cool site'       │     ''      │ 'string'  │
│    1    │   'domain'    │     'example.com'      │     'example.com'      │     ''      │ 'string'  │
│    2    │    'port'     │          8080          │          8080          │     ''      │ 'number'  │
│    3    │ 'databaseUrl' │ 'mongodb://localhost/' │ 'mongodb://localhost/' │     ''      │ 'string'  │
│    4    │    'email'    │         false          │         false          │     ''      │ 'boolean' │
│    5    │ 'emailSender' │       undefined        │       undefined        │     ''      │ 'string?' │
└─────────┴───────────────┴────────────────────────┴────────────────────────┴─────────────┴───────────┘
Modules config
┌─────────┬──────────────────────────────┬─────────────────┬─────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────┐
│ (index) │           name               │      value      │  defaultValue   │                                            description                                             │    type    │
├─────────┼──────────────────────────────┼─────────────────┼─────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────┤
│    0    │       'framework.host'       │   'localhost'   │   'localhost'   │                                                 ''                                                 │  'string'  │
│    1    │       'framework.port'       │      8080       │      8080       │                                                 ''                                                 │  'number'  │
│    2    │    'framework.httpsPort'     │    undefined    │    undefined    │ 'If httpsPort and ssl is defined, then the https server is started additional to the http-server.' │ 'number?'  │
│    3    │    'framework.selfSigned'    │    undefined    │    undefined    │           'If for ssl: true the certificate and key should be automatically generated.'            │ 'boolean?' │
│    4    │ 'framework.keepAliveTimeout' │    undefined    │    undefined    │                                                 ''                                                 │ 'number?'  │
│    5    │       'framework.path'       │       '/'       │       '/'       │                                                 ''                                                 │  'string'  │
│    6    │     'framework.workers'      │        1        │        1        │                                                 ''                                                 │  'number'  │
│    7    │       'framework.ssl'        │      false      │      false      │                                       'Enables HTTPS server'                                       │ 'boolean'  │
│    8    │    'framework.sslOptions'    │    undefined    │    undefined    │                   'Same interface as tls.SecureContextOptions & tls.TlsOptions.'                   │   'any'    │
...

Set Configuration Values

By default, no values are overwritten, so default values are used. There are several ways to set configuration values.

  • Environment variables for each option

  • Environment variable via JSON

  • dotenv files

You can use several methods to load the configuration at the same time. The order in which they are called is important.

Environment Variables

To allow setting each configuration option via its own environment variable, use loadConfigFromEnv. The default prefix is APP_, but you can change it. It also automatically loads .env files. By default it uses an uppercase naming strategy, but you can change that too.

For configuration options like pageTitle above, you can use APP_PAGE_TITLE="Other Title" to change the value.

new App({
    config: config,
    controllers: [MyWebsite],
})
    .loadConfigFromEnv({prefix: 'APP_'})
    .run();
APP_PAGE_TITLE="Other title" ts-node app.ts server:start

JSON Environment Variable

To change multiple configuration options via a single environment variable, use loadConfigFromEnvVariable. The first argument is the name of the environment variable.

new App({
    config: config,
    controllers: [MyWebsite],
})
    .loadConfigFromEnvVariable('APP_CONFIG')
    .run();
APP_CONFIG='{"pageTitle": "Other title"}' ts-node app.ts server:start

DotEnv Files

To change multiple configuration options via a dotenv file, use loadConfigFromEnv. The first argument is either a path to a dotenv (relative to cwd) or multiple paths. If it is an array, each path is tried until an existing file is found.

new App({
    config: config,
    controllers: [MyWebsite],
})
    .loadConfigFromEnv({envFilePath: ['production.dotenv', 'dotenv']})
    .run();
$ cat dotenv
APP_PAGE_TITLE=Other title
$ ts-node app.ts server:start

Module Configuration

Each imported module can have a module name. This name is used for the configuration paths used above.

For example, for configuring environment variables, the path for the FrameworkModule option port is FRAMEWORK_PORT. All names are written in uppercase by default. If a prefix of APP_ is used, the port can be changed via the following:

$ APP_FRAMEWORK_PORT=9999 ts-node app.ts server:start
2021-06-12T18:59:26.363Z [LOG] Start HTTP server, using 1 workers.
2021-06-12T18:59:26.365Z [LOG] HTTP MyWebsite
2021-06-12T18:59:26.366Z [LOG]     GET / helloWorld
2021-06-12T18:59:26.366Z [LOG] HTTP listening at http://localhost:9999/

In dotenv files it would also be APP_FRAMEWORK_PORT=9999.

In JSON environment variables via loadConfigFromEnvVariable('APP_CONFIG') on the other hand, it is the structure of the actual configuration class. framework becomes an object.

$ APP_CONFIG='{"framework": {"port": 9999}}' ts-node app.ts server:start

This works the same for all modules. No module prefix is required for your application configuration option (new App).

Application Server

Public Directory

The FrameworkModule provides a way to serve static files such as images, PDFs, binaries, etc. over HTTP. The publicDir configuration option lets you specify which folder to use as the default entry point for requests that do not lead to an HTTP controller route. By default, this behavior is disabled (empty value).

To enable the provision of public files, set publicDir to a folder of your choice. Normally you would choose a name like publicDir to make things obvious.

.
├── app.ts
└── publicDir
    └── logo.jpg

To change the publicDir option, you can change the first argument of FrameworkModule.

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

// your config and http controller here

new App({
    config: config,
    controllers: [MyWebsite],
    imports: [
        new FrameworkModule({
            publicDir: 'publicDir'
        })
    ]
})
    .run();

All files within this configured folder are now accessible via HTTP. For example, when you open http://localhost:8080/logo.jpg, you see the logo.jpg image in the publicDir directory.

File Structure

Database

Deepkit has its own powerful database abstraction library called Deepkit ORM. It is an Object-Relational Mapping (ORM) library that facilitates work with SQL databases and MongoDB.

Although you can use any database library, we recommend Deepkit ORM as it is the fastest TypeScript database abstraction library that is perfectly integrated with the Deepkit framework and has many features that will improve your workflow and efficiency.

To get all the information about Deepkit ORM, see the Database chapter.

Database Classes

The simplest way to use the Database object of Deepkit ORM within the application is to register a class that derives from it.

import { Database } from '@deepkit/orm';
import { SQLiteDatabaseAdapter } from '@deepkit/sqlite';
import { User } from './models';

export class SQLiteDatabase extends Database {
    name = 'default';
    constructor() {
        super(new SQLiteDatabaseAdapter('/tmp/myapp.sqlite'), [User]);
    }
}

Create a new class and in its constructor specify the adapter with its parameters and add to the second parameter all entities/models that should be connected to this database.

You can now register this database class as a provider. We also enable migrateOnStartup which will automatically create all tables in your database at bootstrap. This is ideal for rapid prototyping, but is not recommended for a serious project or production setup. Normal database migrations should then be used here.

We also enable debug, which allows us to open the debugger when the application’s server is started and manage your database models directly in its built-in ORM browser.

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { SQLiteDatabase } from './database.ts';

new App({
    providers: [SQLiteDatabase],
    imports: [
        new FrameworkModule({
            migrateOnStartup: true,
            debug: true,
        })
    ]
}).run();

You can now access SQLiteDatabase anywhere using Dependency Injection:

import { SQLiteDatabase } from './database.ts';

export class Controller {
    constructor(protected database: SQLiteDatabase) {}

    @http.GET()
    async startPage(): Promise<User[]> {
        //return all users
        return await this.database.query(User).find();
    }
}

More Databases

You can add as many database classes as you like and name them as you like. Be sure to change the name of each database so that it doesn’t conflict with others when you use the ORM browser.

Manage Data

You now have everything set up to manage your database data with the Deepkit ORM Browser. To open the ORM Browser and manage the content, write all the steps from above in the app.ts file and start the server.

$ ts-node app.ts server:start
2021-06-11T15:08:54.330Z [LOG] Start HTTP server, using 1 workers.
2021-06-11T15:08:54.333Z [LOG] Migrate database default
2021-06-11T15:08:54.336Z [LOG] RPC DebugController deepkit/debug/controller
2021-06-11T15:08:54.337Z [LOG] RPC OrmBrowserController orm-browser/controller
2021-06-11T15:08:54.337Z [LOG] HTTP OrmBrowserController
2021-06-11T15:08:54.337Z [LOG]     GET /_orm-browser/query httpQuery
2021-06-11T15:08:54.337Z [LOG] HTTP StaticController
2021-06-11T15:08:54.337Z [LOG]     GET /_debug/:any serviceApp
2021-06-11T15:08:54.337Z [LOG] HTTP listening at http://localhost:8080/
debugger database

You can see the ER diagram. At the moment only one entity is available. If you add more with relationships, you will see all the information at a glance.

If you click on User in the left sidebar, you can manage its content. Click the + icon and change the title of the new record. After you have changed the required values (such as the user name), click Confirm. This will commit all changes to the database and make them permanent. The auto increment ID will be assigned automatically.

debugger database user

Learn More

To learn more about how SQLiteDatabase works, please read the chapter Database and its subchapters, such as querying data, manipulating data via sessions, defining relations and much more. Please note that the chapters there refer to the standalone library @deepkit/orm and do not include documentation about the part of the Deepkit framework you read above in this chapter. In the standalone library, you instantiate your database class manually, for example via new SQLiteDatabase(). However, in your Deepkit framework application, this is done automatically using the Dependency Injection container.

Migration

Logger

Deepkit Logger is a standalone library with a primary Logger class that you can use to log information. This class is automatically deployed in the Dependency Injection container of your Deepkit Framework application.

The Logger class has several methods, each of which behaves like console.log.

Name

Log Level

Level id

logger.error()

Error

1

logger.warning()

Warning

2

logger.log()

Default log

3

logger.info()

Special information

4

logger.debug()

Debug information

5

By default, a logger has info level, i.e. it processes only info messages and more (i.e. log, warning, error, but not debug). To change the log level, call for example logger.level = 5.

Use In The Application

To use the logger in your Deepkit framework application, you can simply inject Logger into your services or controllers.

import { Logger } from '@deepkit/logger';

class MyService {
    constructor(protected logger: Logger) {}

    doSomething() {
        const value = 'yes';
        this.logger.log('This is wild', value);
    }
}

Colors

The logger supports colored log messages. You can provide colors by using XML tags that surround the text you want to appear in color.

const username = 'Peter';
logger.log(`Hi <green>${username}</green>`);

For transporters that do not support colors, the color information is automatically removed. In the default transporter (ConsoleTransport) the color is displayed. The following colors are available: black, red, green, blue, cyan, magenta, white and grey/gray.

Transporter

You can configure a single transporter or multiple transporters. In a Deepkit Framework application, the ConsoleTransport transporter is configured automatically. To configure additional transporters, you can use Setup Calls:

import { Logger, LoggerTransport } from '@deepkit/logger';


export class MyTransport implements LoggerTransport {
    write(message: string, level: LoggerLevel, rawMessage: string) {
        process.stdout.write(JSON.stringify({message: rawMessage, level, time: new Date}) + '\n');
    }

    supportsColor() {
        return false;
    }
}

new App()
    .setup((module, config) => {
        module.setupProvider(Logger).addTransport(new MyTransport);
    })
    .run();

To replace all transporters with a new set of transporters, use setTransport:

import { Logger } from '@deepkit/logger';

new App()
.setup((module, config) => {
    module.setupProvider(Logger).setTransport([new MyTransport]);
})
.run();
import { Logger, JSONTransport } from '@deepkit/logger';

new App()
    .setup((module, config) => {
        module.setupProvider(Logger).setTransport([new JSONTransport]);
    })
    .run();

Formatter

With formatters you can change the message format, e.g. add the timestamp. When an application is started via server:start, a DefaultFormatter is automatically added (which adds timestamp, range and log level) if no other formatter is available.

Scoped Logger

Scoped loggers add an arbitrary area name to each log entry, which can be helpful in determining which subarea of your application the log entry originated from.

const scopedLogger = logger.scoped('database');
scopedLogger.log('Query', query);

JSON Transporter

To change the output to JSON protocols, you can use the supplied JSONTransport.

Context Data

To add contextual data to a log entry, add a simple object literal as the last argument. Only log calls with at least two arguments can contain contextual data.

const query = 'SELECT *';
const user = new User;
logger.log('Query', {query, user}); //last argument is context data
logger.log('Another', 'wild log entry', query, {user}); //last argument is context data

logger.log({query, user}); //this is not handled as context data.

Auto-CRUD

Events

Deepkit framework comes with various event tokens on which event listeners can be registered.

See the Events chapter to learn more about how events work.

Dispatch Events

Events are sent via the EventDispatcher class. In a Deepkit Framework application, this can be provided via dependency injection.

import { cli, Command } from '@deepkit/app';
import { EventDispatcher } from '@deepkit/event';

@cli.controller('test')
export class TestCommand implements Command {
    constructor(protected eventDispatcher: EventDispatcher) {
    }

    async execute() {
        this.eventDispatcher.dispatch(UserAdded, new UserEvent({ username: 'Peter' }));
    }
}

Event Listener

There are two ways to react to events. Either via controller classes or regular functions. Both are registered in the app or in modules under listeners.

Controller Listener

import { eventDispatcher } from '@deepkit/event';

class MyListener {
    @eventDispatcher.listen(UserAdded)
    onUserAdded(event: typeof UserAdded.event) {
        console.log('User added!', event.user.username);
    }
}

new App({
    listeners: [MyListener],
}).run();

Functional Listener

new App({
    listeners: [
        UserAdded.listen((event) => {
            console.log('User added!', event.user.username);
        });
    ],
}).run();

Framework Events

Deepkit Framework itself has several events from the application server that you can listen for.

Functional Listener

import { onServerMainBootstrap } from '@deepkit/framework';
new App({
    listeners: [
        onServerMainBootstrap.listen((event) => {
            console.log('User added!', event.user.username);
        });
    ],
}).run();
Name Description

onServerBootstrap

Called only once for application server bootstrap (for main process and workers).

onServerBootstrapDone

Called only once for application server bootstrap (for main process and workers) as soon as the application server has started.

onServerMainBootstrap

Called only once for application server bootstrap (in the main process).

onServerMainBootstrapDone

Called only once for application server bootstrap (in the main process) as soon as the application server has started

onServerWorkerBootstrap

Called only once for application server bootstrap (in the worker process).

onServerWorkerBootstrapDone

Called only once for application server bootstrap (in the worker process) as soon as the application server has started.

ServerShutdownEvent

Called when application server shuts down (in master process and each worker).

onServerMainShutdown

Called when application server shuts down in the main process.

onServerWorkerShutdown

Called when application server shuts down in the worker process.

Deployment

In this chapter, you will learn how to compile your application in JavaScript, configure it for your production environment, and deploy it using Docker.

Compile TypeScript

Suppose you have an application like this in an app.ts file:

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

class Config {
    title: string = 'DEV my Page';
}

class MyWebsite {
    constructor(protected title: Config['title']) {
    }

    @http.GET()
    helloWorld() {
        return 'Hello from ' + this.title;
    }
}

new App({
    config: Config,
    controllers: [MyWebsite],
    imports: [new FrameworkModule]
})
    .loadConfigFromEnv()
    .run();

If you use ts-node app.ts server:start, you will see that everything works correctly. In a production environment, you would not typically start the server with ts-node. You would compile it into JavaScript and then use the node. To do this, you must have a correct tsconfig.json with the correct configuration options. In the "First Application" section, your tsconfig.json is configured to output JavaScript to the ./dist folder. We assume that you have configured it that way as well.

If all compiler settings are correct and your outDir points to a folder like dist, then as soon as you run the tsc command in your project, all your linked files in the files in the tsconfig.json will be compiled to JavaScript. It is enough to specify your entry files in this list. All imported files are also compiled automatically and do not need to be explicitly added to tsconfig.json. tsc is part of Typescript when you install npm install typescript.

$ ./node_modules/.bin/tsc

The TypeScript compiler does not output anything if it was successful. You can now check the output of dist.

$ tree dist
dist
└── app.js

You can see that there is only one file. You can run it from node dist/app.js and get the same functionality as with ts-node app.ts.

For a deployment, it is important that the TypeScript files are compiled correctly and everything works directly through Node. You could now simply move your dist folder including your node_modules and run node dist/app.js server:start and your app is successfully deployed. However, you would use other solutions like Docker to package your app correctly.

Configuration

In a production environment, you would not bind the server to localhost, but most likely to all devices via 0.0.0.0. If you are not behind a reverse proxy, you would also set the port to 80. To configure these two settings, you need to customize the FrameworkModule. The two options we are interested in are host and port. In order for them to be configured externally via environment variables or via .dotenv files, we must first allow this. Fortunately, our code above has already done this with the loadConfigFromEnv() method.

Please refer to the configuration chapter to learn more about how to set the application configuration options.

To see what configuration options are available and what value they have, you can use the ts-node app.ts app:config command. You can also see them in the Framework Debugger.

SSL

It is recommended (and sometimes required) to run your application over HTTPS with SSL. There are several options for configuring SSL. To enable SSL, use framework.ssl and configure its parameters with the following options.

Name Type Description

framework.ssl

boolean

Enables HTTPS server when true

framework.httpsPort

number?

If httpsPort and ssl is defined, then the https server is started additional to the http server.

framework.sslKey

string?

A file path to a ssl key file for https

framework.sslCertificate

string?

A file path to a certificate file for https

framework.sslCa

string?

A file path to a ca file for https

framework.sslCrl

string?

A file path to a crl file for https

framework.sslOptions

object?

Same interface as tls.SecureContextOptions & tls.TlsOptions.

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

// your config and http controller here

new App({
    config: Config,
    controllers: [MyWebsite],
    imports: [
        new FrameworkModule({
            ssl: true,
            selfSigned: true,
            sslKey: __dirname + 'path/ssl.key',
            sslCertificate: __dirname + 'path/ssl.cert',
            sslCA: __dirname + 'path/ssl.ca',
        })
    ]
})
    .run();

Local SSL

In the local development environment, you can enable self-signed HTTPs with the framework.selfSigned option.

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

// your config and http controller here

new App({
    config: config,
    controllers: [MyWebsite],
    imports: [
        new FrameworkModule({
            ssl: true,
            selfSigned: true,
        })
    ]
})
    .run();
$ ts-node app.ts server:start
2021-06-13T18:04:01.563Z [LOG] Start HTTP server, using 1 workers.
2021-06-13T18:04:01.598Z [LOG] Self signed certificate for localhost created at var/self-signed-localhost.cert
2021-06-13T18:04:01.598Z [LOG] Tip: If you want to open this server via chrome for localhost, use chrome://flags/#allow-insecure-localhost
2021-06-13T18:04:01.606Z [LOG] HTTP MyWebsite
2021-06-13T18:04:01.606Z [LOG]     GET / helloWorld
2021-06-13T18:04:01.606Z [LOG] HTTPS listening at https://localhost:8080/

If you start this server now, your HTTP server is available as HTTPS at https://localhost:8080/. In Chrome, you now get the error message "NET::ERR_CERT_INVALID" when you open this URL because self-signed certificates are considered a security risk: chrome://flags/#allow-insecure-localhost.

Testing

The services and controllers in the Deepkit framework are designed to support SOLID and clean code that is well designed, encapsulated, and separated. These features make the code easy to test.

This documentation shows you how to set up a testing framework called Jest with ts-jest. To do this, run the following command to install jest and ts-jest.

npm install jest ts-jest @types/jest

Jest needs a few configuration options to know where to find the test suits and how to compile the TS code. Add the following configuration to your package.json:

{
  ...,

  "jest": {
    "transform": {
      "^.+\\.(ts|tsx)$": "ts-jest"
    },
    "testEnvironment": "node",
    "resolver": "@deepkit/framework/resolve",
    "testMatch": [
      "**/*.spec.ts"
    ]
  }
}

Your test files should be named *.spec.ts. Create a test.spec.ts file with the following content.

test('first test', () => {
    expect(1 + 1).toBe(2);
});

You can now use the jest command to run all your test suits at once.

$ node_modules/.bin/jest
 PASS  ./test.spec.ts
  ✓ first test (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.23 s, estimated 1 s
Ran all test suites.

Please read the Jest documentation to learn more about how the Jest CLI tool works and how you can write more sophisticated tests and entire test suites.

Unit Test

Whenever possible, you should unit test your services. The simpler, better separated, and better defined your service dependencies are, the easier it is to test them. In this case, you can write simple tests like the following:

export class MyService {
    helloWorld() {
        return 'hello world';
    }
}
//
import { MyService } from './my-service.ts';

test('hello world', () => {
    const myService = new MyService();
    expect(myService.helloWorld()).toBe('hello world');
});

Integration Tests

It’s not always possible to write unit tests, nor is it always the most efficient way to cover business-critical code and behavior. Especially if your architecture is very complex, it is beneficial to be able to easily perform end-to-end integration tests.

As you have already learned in the Dependency Injection chapter, the Dependency Injection Container is the heart of Deepkit. This is where all services are built and run. Your application defines services (providers), controllers, listeners, and imports. For integration testing, you don’t necessarily want to have all services available in a test case, but you usually want to have a stripped down version of the application available to test the critical areas.

import { createTestingApp } from '@deepkit/framework';
import { http, HttpRequest } from '@deepkit/http';

test('http controller', async () => {
    class MyController {

        @http.GET()
        hello(@http.query() text: string) {
            return 'hello ' + text;
        }
    }

    const testing = createTestingApp({ controllers: [MyController] });
    await testing.startServer();

    const response = await testing.request(HttpRequest.GET('/').query({text: 'world'}));

    expect(response.getHeader('content-type')).toBe('text/plain; charset=utf-8');
    expect(response.body.toString()).toBe('hello world');
});
import { createTestingApp } from '@deepkit/framework';

test('service', async () => {
    class MyService {
        helloWorld() {
            return 'hello world';
        }
    }

    const testing = createTestingApp({ providers: [MyService] });

    //access the dependency injection container and instantiate MyService
    const myService = testing.app.get(MyService);

    expect(myService.helloWorld()).toBe('hello world');
});

If you have divided your application into several modules, you can test them more easily. For example, suppose you have created an AppCoreModule and want to test some services.

class Config {
    items: number = 10;
}

export class MyService {
    constructor(protected items: Config['items']) {

    }

    doIt(): boolean {
        //do something
        return true;
    }
}

export AppCoreModule = new AppModule({
    config: config,
    provides: [MyService]
}, 'core');

You use your module as follows:

import { AppCoreModule } from './app-core.ts';

new App({
    imports: [new AppCoreModule]
}).run();

And test it without booting the entire application server.

import { createTestingApp } from '@deepkit/framework';
import { AppCoreModule, MyService } from './app-core.ts';

test('service simple', async () => {
    const testing = createTestingApp({ imports: [new AppCoreModule] });

    const myService = testing.app.get(MyService);
    expect(myService.doIt()).toBe(true);
});

test('service simple big', async () => {
    // you change configurations of your module for specific test scenarios
    const testing = createTestingApp({
        imports: [new AppCoreModule({items: 100})]
    });

    const myService = testing.app.get(MyService);
    expect(myService.doIt()).toBe(true);
});