Context
Ts.ED provides an utility to get request, response, to store and share data along all middlewares/endpoints during a request withPlatformContext. This context is created by Ts.ED when the request is handled by the server.
It contains some information as following:
- The request and response abstraction layer withPlatformRequest andPlatformResponse (since v5.64.0+),
- The request id,
- The request container used by the Ts.ED DI. It contains all services annotated with
@Scope(ProviderScope.REQUEST), - The currentEndpointMetadata context resolved by Ts.ED during the request,
- The data returned by the previous endpoint if you use multiple handlers on the same route. By default, data is empty.
- TheContextLogger to log some information related to the request and his id.
Here is an example:
import {Context}from "@tsed/platform-params";import {Middleware, UseBefore}from "@tsed/platform-middlewares";import {Get}from "@tsed/schema";import {Controller}from "@tsed/di";import {Forbidden}from "@tsed/exceptions";import {AuthToken}from "../domain/auth/AuthToken.js";@Middleware()class AuthTokenMiddleware { use(@Context()ctx: Context) { if (!ctx.has("auth")) { ctx.set("auth",new AuthToken(ctx.request)); } try { ctx.get("auth").claims();// check token }catch (er) { throw new Forbidden("Access forbidden - Bad token"); } }}@Controller("/")@UseBefore(AuthTokenMiddleware)// protect all routes for this controllerclass MyCtrl { @Get("/") get(@Context()context: Context, @Context("auth")auth: AuthToken) { context.logger.info({event:"auth", auth});// Attach log to the request }}TIP
ContextLogger is attached to the contextctx.logger. The ContextLogger stores all logs and Ts.ED prints ( flushes) all logs after the response is sent by the server. The approach optimizes performance by first sending in the response and then printing all logs.
Endpoint metadata
EndpointMetadata is the current controller method executed by the request. Middleware can access to this endpoint metadata when you use the middleware over a controller method. By accessing to theEndpointMetadata you are able to:
- Get endpoint information,
- Createcustom middleware and decorator,
- Get the controller class name and propertyKey.
import {StoreSet}from "@tsed/core";import {Controller}from "@tsed/di";import {Middleware, Use}from "@tsed/platform-middlewares";import {Context}from "@tsed/platform-params";import {Get, Returns}from "@tsed/schema";import {Resource}from "./Resource";@Middleware()export class MyMiddleware { use(@Context()ctx: Context) { console.log(ctx.endpoint);// Endpoint Metadata console.log(ctx.endpoint.targetName);// MyCtrl console.log(ctx.endpoint.propertyKey);// getMethod console.log(ctx.endpoint.type);// Resource console.log(ctx.endpoint.store.get("options"));// options }}@Controller("/resources")class MyCtrl { @Get("/:id") @Use(MyMiddleware) @Returns(200, Resource) @StoreSet("options","options") getMethod(): Resource { return new Resource(); }}Request and Response abstraction
PlatformContext provide aPlatformRequest andPlatformResponse classes which are an abstraction layer of the targeted platform (Express.js, Koa.js, etc...).
By using the PlatformContext interface, your code will be compatible with any platform. But, the abstraction doesn't or cannot provide all necessaries properties or methods. It's also possible to get the original request or response by different ways.
import {Req, Res}from "@tsed/platform-http";import {Middleware}from "@tsed/platform-middlewares";import {Context}from "@tsed/platform-params";@Middleware()export class MyMiddleware { use(@Req()req: Req, @Res()res: Res, @Context()ctx: Context) { // abstraction console.log(ctx.request);// PlatformRequest console.log(ctx.response);// PlatformResponse // by decorator console.log(req);// Express.Request console.log(res);// Express.Response // by console.log(ctx.request.raw);// Express.Request console.log(ctx.response.raw);// Express.Request // by method console.log(ctx.getRequest<Express.Request>());// Express.Request console.log(ctx.getResponse<Express.Response>());// Express.Response }}PlatformRequest
PlatformRequest provide high level methods and properties to get request information. His interface is the following:
interface PlatformRequest<T = Req> { raw: T; get secure(): boolean; get url(): string; get route(): string; get headers(): IncomingHttpHeaders; get method(): string; get body(): {[key: string]: any}; get rawBody(): {[key: string]: any}; get cookies(): {[key: string]: any}; get params(): {[key: string]: any}; get query(): {[key: string]: any}; get session(): {[key: string]: any}| undefined; get(name: string): string | undefined;// get header getHeader(name: string): string | undefined;// get header accepts(mime?: string | string[]): string | string[]| false; isAborted(): boolean;}Get request headers
import {Controller}from "@tsed/di";import {Context}from "@tsed/platform-params";@Controller("/")export class MyController { @Get("/") get(@Context()ctx: Context) { ctx.request.headers;// return all headers ctx.get("host");// return host header ctx.getHeader("host");// return host header }}Get request params/body/query/cookies/session
import {Controller}from "@tsed/di";import {Context}from "@tsed/platform-params";@Controller("/")export class MyController { @Get("/") get(@Context()ctx: Context) { ctx.request.body;// return body payload ctx.request.params;// return path params ctx.request.query;// return query params ctx.request.cookies;// return cookies ctx.request.session;// return session }}PlatformResponse
PlatformResponse provide high level methods like.body() to send any data to your consumer. This method can getBoolean,Number,String,Date,Object,Array orStream as input type and determine the correct way to send the response to your consumer.
His interface is the following:
interface PlatformResponse { raw: Res; get statusCode(): number; get locals(): Record<string,any>; status(status: number): this; setHeaders(headers: {[key: string]: any}): this; contentType(contentType: string): this; redirect(status: number,url: string): this; location(location: string): this; stream(data: ReadableStream | any): this; render(path: string,options?: any): Promise<string>; body(data: any): this; onEnd(cb: Function): void; destroy(): void; cookie(name: string,value: string | null,opts?: TsED.SetCookieOpts): this;}Set response headers
import {Controller}from "@tsed/di";import {Context}from "@tsed/platform-params";@Controller("/")export class MyController { @Get("/") get(@Context()ctx: Context) { // set headers, content-type and status ctx.response.setHeaders({"x-header":"header"}); ctx.response.contentType("application/json"); ctx.response.status(201); }}Can be also done by returning a response like object:
import {Controller}from "@tsed/di";import {Context}from "@tsed/platform-params";@Controller("/")export class MyController { @Get("/") get(@Context()ctx: Context) { return { statusText:"OK", status:200, headers: {}, data: {} }; }}Set response cookie
import {Controller}from "@tsed/di";import {Context}from "@tsed/platform-params";@Controller("/")export class MyController { @Get("/") get(@Context()ctx: Context) { // set ctx.response.cookie("locale","fr-FR"); // clear ctx.response.cookie("locale",null); }}Set response body
import {Controller}from "@tsed/di";import {Context}from "@tsed/platform-params";@Controller("/")export class MyController { @Get("/") get(@Context()ctx: Context) { // equivalent to ctx.getResponse().send() ctx.response.body(null); ctx.response.body(undefined); ctx.response.body(true); ctx.response.body(false); // equivalent to ctx.getResponse().json() ctx.response.body({}); ctx.response.body([]); ctx.response.body(new Date()); // equivalent to readableStream.pipe(ctx.response.raw) ctx.response.body(readableStream); }}But prefer returning payload from your method! Ts.ED will handle all data type (Buffer/Stream/Data/Promise/Observable).
Manipulate original response
You can retrieve the Express\Koa response by usingctx.getResponse() method:
import {Controller}from "@tsed/di";import {Context}from "@tsed/platform-params";@Controller("/")export class MyController { @Get("/") get(@Context()ctx: Context) { // Express.js ctx.getResponse<Express.Response>().status(201).send("Hello"); }}Inject context
InjectPlatformContext from a controller and forward the context to another service could be a pain point. See example:
@Injectable()export class CustomRepository { async findById(id: string,ctx: PlatformContext) { ctx.logger.info("Where are in the repository"); return { id, headers:this.$ctx?.request.headers }; }}@Controller("/async-hooks")export class AsyncHookCtrl { @Inject() repository: CustomRepository; @Get("/:id") async get(@PathParams("id")id: string, @Context()ctx: PlatformContext) { return this.repository.findById(id, ctx); }}Withv6.26.0, Ts.ED provide a way to get thePlatformContext directly from a Service called by a controller, using theAsyncLocalStorage provided by Node.js.
This feature is experimental but in reality, the API is stable and the benefit to use it is here!
If you have a Ts.ED version under v6.113.0, you have to install the@tsed/async-hook-context package if your environment have the required Node.js (v13+) version.
npm install --save @tsed/async-hook-contextSincev6.113.0, theAsyncLocalStorage is automatically enabled.
With this feature, you can inject directly thePlatformContext in the service without injecting it in the controller:
import {Injectable, Controller, InjectContext}from "@tsed/di";import {PlatformContext}from "@tsed/platform-http";@Injectable()export class CustomRepository { @InjectContext() protected $ctx?: PlatformContext; async findById(id: string) { this.ctx?.logger.info("Where are in the repository"); return { id, headers:this.$ctx?.request.headers }; }}@Controller("/async-hooks")export class AsyncHookCtrl { @Inject() private readonly repository: CustomRepository; @Get("/:id") async get(@PathParams("id")id: string) { return this.repository.findById(id); }}import {context, controller, injectable}from "@tsed/di";import {PlatformContext}from "@tsed/platform-http";import {PathParams}from "@tsed/platform-params";import {Get}from "@tsed/schema";export class CustomRepository { async findById(id: string) { const $ctx = context<PlatformContext>(); $ctx?.logger.info("Where are in the repository"); return { id, headers: $ctx?.request.headers }; }}injectable(CustomRepository);export class AsyncHookCtrl { private readonly repository = inject(CustomRepository); @Get("/:id") async get(@PathParams("id")id: string) { return this.repository.findById(id); }}controller(AsyncHookCtrl).path("/async-hooks");To run a method with context in your unit test, you can use therunInContext function.
import {inject, runInContext}from "@tsed/di";import {PlatformTest}from "@tsed/platform-http/testing";import {CustomRepository}from "./CustomRepository";describe("CustomRepository", ()=> { beforeEach(()=> PlatformTest.create()); afterEach(()=> PlatformTest.reset()); it("should run method with the ctx",async ()=> { const ctx = PlatformTest.createRequestContext(); const service = await PlatformTest.invoke(CustomRepository, []); ctx.request.headers= { "x-api":"api" }; const result = await runInContext(ctx, ()=> service.findById("id")); expect(result).toEqual({ id:"id", headers: { "x-api":"api" } }); });});