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
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) {
}
}
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();

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.
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/
You can now open http://localhost:8080/_debug/database/default.

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.

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