Skip to main content

Examples

In this article, we will show you what a SenseJS application looks like with two simple examples.

The code of the examples in this article can be found at examples folder in the SenseJS repository.

Set up

To run the example from the SenseJS repository, you need to install the dependencies first.

Note that the SenseJS repository uses pnpm as the package manager, so you should run the following

pnpm i -r

to install the dependencies.

However, if you would like to write the code from scratch, you need to set up a Node.js project with the following packages installed.

  • reflect-metadata, @sensejs/http, @sensejs/core. These packages are required to run this example.

  • typescript. It should be the dev dependency of your project unless you have it installed globally.

  • Optionally include ts-node in your dev dependencies, we'll use it to run the demo. you can also compile the source file manually before running the app.

Also, you need to configure the tsconfig.json, as instructed in the previous article.

Hello world

There is only a single file named main.ts in this example, with the following content.

import 'reflect-metadata';
import {createKoaHttpModule, Controller, GET} from '@sensejs/http';
import {ApplicationRunner, ModuleClass, OnModuleCreate} from '@sensejs/core';

@Controller('/')
class HelloWorldController {

@GET('/')
helloWorld() {
return 'hello world';
}

}

@ModuleClass({
requires: [
createKoaHttpModule({
components: [HelloWorldController],
httpOption: {
listenAddress: 'localhost',
listenPort: 8080,
}
})
]
})
class HelloWorldApp {

@OnModuleStart()
onModuleCreate() {
console.log('service started');
}
}

ApplicationRunner.instance.start(HelloWorldApp);

You can run this simple http service via

ts-node main.ts

The above code create simple HTTP service that will listen at localhost:8080.

After starting it, you shall be able to visit http://localhost:8080/ with an HTTP client, e.g. curl, to see the output from this app.

$ curl localhost:8080
hello world

Each time we send an HTTP request to http://localhost:8080/, an instance of HelloWorldController will be instantiated and the method helloWorld will be invoked, and the return value will be sent back to the HTTP client.

Dependency injection

In this example, we will show you how dependency injection works.

The code of this example can be found at ./examples/injection

In this example, we separate the code into three parts.

  • random-number.ts: contains a simple component RandomNumberGenerator and a controller RandomNumberController for querying or mutating the state of RandomNumberGenerator, and exporting it as a module RandomNumberModule.

  • http.module.ts: containing the code for setting up an HTTP server, including all middleware

  • index.ts: the entry point of the application, which imports RandomNumberModule and HttpModule and start the application.

RandomNumberModule

In this section we focused on file random-number.module.ts

@Component()
@Scope(Scope.SINGLETON)
class RandomNumberGenerator {

private state: number = Date.now() >>> 0; // Truncate the value of Date.now() into a 32-bit integer

reseed(seed: number) {
this.state = seed >>>= 0;
return this.state;
}

query() {
return this.state;
}

next() {
this.state = (this.state * 64829 + 0x5555) >>> 0;
return this.state;
}
}

As you see, the class RandomNumberGenerator is decorated with @Component(), which makes it an injectable component.


@Controller('/')
class RandomNumberController {

constructor(@Inject(RandomNumberGenerator) private generator: RandomNumberGenerator,
@InjectLogger() private logger: Logger) {}

@GET('state')
async get() {
const state = this.generator.query();
return {state};
}

@POST('next')
async nextRandom() {
const value = this.generator.next();
this.logger.info('Generated random number: ', value);
return {value};
}

@POST('reseed')
async reseed(@Body() body: any) {
const seed = Number(body?.seed);
if (!Number.isInteger(seed)) {
this.logger.warn('Invalid seed %s, ignored', seed);
} else {
this.generator.reseed(seed);
}
return {state: this.generator.query()};
}
}

The above class provides an HTTP controller to query or mutate the state of RandomNumberGenerator, its constructor has two parameters.

  • the first one requires an instance of RandomNumberGenerator, which is defined previously,
  • and the second one requires an instance of Logger.

They will be instantiated and injected automatically when the controller is instantiated by the framework.

When handling requests, the framework will instantiate an instance of RandomNumberController, and invoke the appropriate method, and if the method needs parameters, the framework will inject them automatically based on the decorator of each parameter.

For example, when handling request of POST /reseed, the request body will be injected as the parameter to the reseed method.

At the end of this file, RandomNumberGenerator and RandomNumberController are packaged into a module RandomNumberModule.


export const RandomNumberModule = createModule({
components: [RandomNumberGenerator, RandomNumberController]
});

HttpModules

In this section, we focused on another file ./src/http.module.ts.

We'll explain the content of this file in reverse order.

At the end of this file, a module is created by createKoaHttpModule, just like what we did in the hello world example, but this time two middlewares are added.

export const HttpModule = createKoaHttpModule({
// We need to list RandomNumberModule here so that RandomNumberController can be discovered
requires: [SenseLogModule, RandomNumberModule],

// The order must not be changed, since REQUEST_ID is not defined before RequestIdMiddleware
middlewares: [
RequestIdMiddleware,
ContextualLoggingMiddleware
],

httpOption: {
listenAddress: 'localhost',
listenPort: 8080,
},
});

There are two middleware defined prior to the HTTP module.

The first one, RequestIdMiddleware assigns a request-id to each request, and bound it to a symbol REQUEST_ID:

import {randomUUID} from 'crypto';

const REQUEST_ID = Symbol('REQUEST_ID');

@Middleware({
provides: [REQUEST_ID]
})
class RequestIdMiddleware {

async intercept(next: (requestId: string) => Promise<void>) {
const requestId = randomUUID();
// The parameter passed to next() will be bound to REQUEST_ID
await next(requestId);
}
}

The second one, ContextualLoggingMiddleware injects the request-id bound in previous middleware and attaches it to a logger builder, and in fact it overrides the LoggerBuilder in this request, so all logger created in this request will share the same request-id, and their output can be grouped by the request-id easily. This is very useful when you want to distinguish the logs from different concurrent requests.


@Middleware({
provides: [LoggerBuilder]
})
class ContextualLoggingMiddleware {

constructor(
// It'll be injected with a value provided by the previous interceptor
@Inject(REQUEST_ID) private requestId: string,
// It'll be injected with the LoggerBuilder defined in the global
@InjectLogger() private logger: Logger
) {}

async intercept(next: (lb: LoggerBuilder) => Promise<void>) {
this.logger.debug('Associate LoggerBuilder with requestId=%s', this.requestId);
const slb = defaultLoggerBuilder.setTraceId(this.requestId);
// The parameter passed to next() will be bound to LoggerBuilder
await next(slb);
}

}

Entrypoint

In the entry file, we need to import "reflect-metadata" at the first place. Then we just create a module and mark it as an entrypoint.

import 'reflect-metadata';
import {EntryPoint, ModuleClass} from '@sensejs/core';
import {HttpModule} from './http.js';

@EntryPoint()
@ModuleClass({
requires: [
HttpModule
],
})
class App {
}

That's it.

Running

You can run this app and send requests with curl, you'll see output like this

% curl http://localhost:8080/state
{"state":4005820056}

% curl http://localhost:8080/next -XPOST
{"value":2405846925}

% curl http://localhost:8080/next -XPOST
{"value":1207935726}

% curl http://localhost:8080/reseed -d 'seed=1111'
{"state":1111}

% curl http://localhost:8080/reseed -d 'seed=invalid'
{"state":1111}

curl http://localhost:8080/next -XPOST
{"value":72046864}

On the application log, you'll see something like

+ 16:51:05.494 ContextualLoggingMiddleware - | Associate LoggerBuilder with requestId=25c469ea-2c9f-4ade-9d1f-a2603e509402
+ 16:51:09.609 ContextualLoggingMiddleware - | Associate LoggerBuilder with requestId=19ad7258-08b6-4fec-8d0b-042067fa5bf8
+ 16:51:09.609 RandomNumberController 19ad7258-08b6-4fec-8d0b-042067fa5bf8 | Generated random number: 2405846925
+ 16:51:11.922 ContextualLoggingMiddleware - | Associate LoggerBuilder with requestId=9b9c909b-ba79-48f2-8fa4-febd39dc781f
+ 16:51:11.923 RandomNumberController 9b9c909b-ba79-48f2-8fa4-febd39dc781f | Generated random number: 1207935726
+ 16:51:16.972 ContextualLoggingMiddleware - | Associate LoggerBuilder with requestId=fa3c6df8-ccca-48d4-85ba-88520ca98986
+ 16:51:20.076 ContextualLoggingMiddleware - | Associate LoggerBuilder with requestId=7d840e09-f95d-48e2-b398-e60cf192e801
+ 16:51:20.077 RandomNumberController 7d840e09-f95d-48e2-b398-e60cf192e801 | Invalid seed NaN, ignored
+ 16:51:22.194 ContextualLoggingMiddleware - | Associate LoggerBuilder with requestId=67ce037b-5d64-4a16-a57d-fba78ceed8f8
+ 16:51:22.194 RandomNumberController 67ce037b-5d64-4a16-a57d-fba78ceed8f8 | Generated random number: 72046864