Creating a Custom Platform Adapter
Ts.ED provides built-in support for several web frameworks like Express.js, Koa.js, and Fastify through its Platform Adapter system. This guide explains how to create your own custom adapter to integrate any web framework with Ts.ED.
Understanding Platform Adapters
Platform adapters serve as a bridge between Ts.ED's platform-agnostic abstractions and specific web frameworks. They allow your application code to remain framework-agnostic while providing the necessary integration with your chosen web framework.
Ts.ED provides an abstraction layer based on decorators from @tsed/schema
. These decorators create an abstraction model that allows the @tsed/platform-routers
module to generate a routing table that can be mapped to any target framework (Express.js, Koa.js, Fastify.js, etc.).
The routing table contains all the necessary information to map HTTP verbs, paths, and handlers to the target framework. The adapter's primary responsibility is to perform this mapping, transforming Ts.ED handlers into handlers that are compatible with the target framework.
Target frameworks generally provide similar functionality, but when certain features are missing in a particular framework, it's the adapter's responsibility to fill these gaps.
The core of this system is the PlatformAdapter abstract class, which your custom adapter will extend.
Adapter Implementation Steps
1. Project Structure
Create a new package with the following structure:
platform-yourframework/
├── src/
│ ├── components/
│ │ └── PlatformYourFramework.ts
│ ├── interfaces/
│ │ └── PlatformYourFrameworkSettings.ts
│ ├── services/
│ │ ├── PlatformYourFrameworkHandler.ts
│ │ ├── PlatformYourFrameworkRequest.ts
│ │ └── PlatformYourFrameworkResponse.ts
│ ├── middlewares/
│ │ └── staticsMiddleware.ts
│ └── index.ts
└── package.json
You can take inspiration from the existing adapters in the Ts.ED repository, such as:
2. Extend PlatformAdapter
Create your adapter class by extending PlatformAdapter:
import {adapter, PlatformAdapter, PlatformHandler, PlatformResponse} from "@tsed/platform-http";
import YourFramework from "your-framework";
export class PlatformYourFramework extends PlatformAdapter<YourFramework.Application> {
readonly NAME = "yourframework";
// Implementation of required methods
// ...
}
// Register your adapter and its services
adapter(PlatformYourFramework, [
{
token: PlatformResponse,
useClass: PlatformYourFrameworkResponse
},
{
token: PlatformHandler,
useClass: PlatformYourFrameworkHandler
}
]);
3. Implement Required Methods
Your adapter must implement several key methods:
createApp()
Creates and configures the framework application instance:
import {PlatformAdapter} from "@tsed/platform-http";
class PlatformYourFramework extends PlatformAdapter {
createApp() {
const app = new YourFramework();
// Configure app with any default settings
return {
app,
callback: () => app
};
}
}
class PlatformExpress extends PlatformAdapter {
createApp() {
const app = constant<Express.Express>("express.app") || Express();
return {
app,
callback: () => app
};
}
}
import {PlatformAdapter} from "@tsed/platform-http";
class PlatformKoa extends PlatformAdapter {
createApp() {
const app = constant<Koa | undefined>("koa.app") || new Koa();
koaQs(app, "extended");
return {
app,
callback() {
return app.callback();
}
};
}
}
import {PlatformAdapter} from "@tsed/platform-http";
import type {FastifyInstance} from "fastify";
class PlatformFastify extends PlatformAdapter {
createApp() {
const {app, ...props} = constant<PlatformFastifySettings>("fastify") || {};
// Configure options...
const instance: FastifyInstance = app || Fastify(opts);
instance.decorateRequest("$ctx", null as never);
instance.decorateReply("locals", null);
return {
app: instance,
callback: () => {
return async (request, response) => {
await instance.ready();
instance.server.emit("request", request, response);
};
}
};
}
}
useContext()
This method is essential to the framework's operation. It enables the transportation of the $ctx
object across all methods executed from the controller to the service layer.
This system relies on Node.js's async_hooks
feature, which allows context to be maintained throughout asynchronous operations.
Without this feature properly implemented, Ts.ED's functionality would be degraded or completely non-functional, as the context that carries request-specific data would be lost between middleware and controller executions.
Sets up the context for each request:
class PlatformYourFramework extends PlatformAdapter {
useContext(): void {
const invoke = createContext();
const app = application();
// Add middleware to create context for each request
app.use(async (request, response, next) => {
const $ctx = invoke({request, response});
await $ctx.start();
// Store context on request for later use
request.$ctx = $ctx;
// Clean up when response is finished
response.on("finish", () => $ctx.finish());
return next();
});
}
}
mapHandler()
Maps Ts.ED handlers to the target framework handler convention:
class PlatformYourFramework extends PlatformAdapter {
mapHandler(handler: Function, metadata: PlatformHandlerMetadata) {
// For error middleware
if (metadata.type == PlatformHandlerType.ERR_MIDDLEWARE) {
return (error, req, res, next) => {
// signature may vary by framework
// Handle error middleware
// ...
};
}
// For regular handler
return (req, res, next) => {
// signature may vary by framework
// Execute handler in context
// ...
};
}
}
import {PlatformAdapter} from "@tsed/platform-http";
import {PlatformHandlerMetadata, PlatformHandlerType} from "@tsed/platform-router";
import {runInContext} from "@tsed/di";
import {catchAsyncError} from "@tsed/core";
class PlatformExpress extends PlatformAdapter {
mapHandler(handler: Function, metadata: PlatformHandlerMetadata) {
if (metadata.type == PlatformHandlerType.ERR_MIDDLEWARE) {
return (error: unknown, req: any, res: any, next: any) => {
return runInContext(req.$ctx, async () => {
const {$ctx} = req;
$ctx.next = next;
$ctx.error = error;
$ctx.error = await catchAsyncError(() => handler($ctx));
return callNext(next, metadata, $ctx);
});
};
}
return (req: any, res: any, next: any) => {
return runInContext(req.$ctx, async () => {
const {$ctx} = req;
$ctx.next = next;
$ctx.error = await catchAsyncError(() => handler($ctx));
return callNext(next, metadata, $ctx);
});
};
}
}
import {PlatformAdapter} from "@tsed/platform-http";
import {PlatformHandlerMetadata, PlatformHandlerType} from "@tsed/platform-router";
import Koa from "koa";
import {catchAsyncError} from "@tsed/core";
class PlatformKoa extends PlatformAdapter {
mapHandler(handler: Function, metadata: PlatformHandlerMetadata) {
return async (koaContext: Koa.Context, next: Koa.Next) => {
const {$ctx} = koaContext.request;
$ctx.next = next;
const error = await catchAsyncError(() => handler($ctx));
if (error) {
$ctx.error = error;
}
if (metadata.type !== PlatformHandlerType.RESPONSE_FN) {
return $ctx.next && $ctx.error ? $ctx.next($ctx.error) : $ctx.next();
}
};
}
}
import {PlatformAdapter} from "@tsed/platform-http";
import {PlatformHandlerMetadata, PlatformHandlerType} from "@tsed/platform-router";
import type {IncomingMessage} from "node:http";
import {runInContext} from "@tsed/di";
import type {FastifyRequest} from "fastify";
class PlatformFastify extends PlatformAdapter {
mapHandler(handler: (...args: any[]) => any, metadata: PlatformHandlerMetadata) {
if (metadata.isRawMiddleware()) {
return handler;
}
switch (metadata.type) {
case PlatformHandlerType.MIDDLEWARE:
return (request: IncomingMessage, _: ServerResponse, done: (err?: any) => void) => {
const {$ctx} = request;
$ctx.next = done;
return runInContext($ctx, () => handler($ctx));
};
default:
return async (request: FastifyRequest, _: FastifyReply, done: (err?: any) => void) => {
const {$ctx} = request;
$ctx.next = done;
await runInContext($ctx, () => handler($ctx));
if (metadata.type === PlatformHandlerType.CTX_FN) {
done();
}
};
}
}
}
mapLayers()
Maps platform layers to framework routes:
class PlatformYourFramework extends PlatformAdapter {
mapLayers(layers: PlatformLayer[]) {
const rawApp = this.app.getApp();
layers.forEach((layer) => {
switch (layer.method) {
case "statics":
// Handle static files
rawApp.use(layer.path, this.statics(layer.path as string, layer.opts as any));
return;
default:
// Handle routes
rawApp[layer.method](...layer.getArgs());
}
});
}
}
import {PlatformAdapter} from "@tsed/platform-http";
class PlatformExpress extends PlatformAdapter {
mapLayers(layers: PlatformLayer[]) {
const rawApp: any = this.app.getApp();
layers.forEach((layer) => {
switch (layer.method) {
case "statics":
rawApp.use(layer.path, this.statics(layer.path as string, layer.opts as any));
return;
}
rawApp[layer.method](...layer.getArgs());
});
}
}
import {PlatformAdapter} from "@tsed/platform-http";
class PlatformKoa extends PlatformAdapter {
mapLayers(layers: PlatformLayer[]) {
const options = constant("koa.router", {});
const rawRouter = new KoaRouter(options) as any;
layers.forEach((layer) => {
switch (layer.method) {
case "statics":
rawRouter.use(layer.path, this.statics(layer.path as string, layer.opts as any));
break;
default:
rawRouter[layer.method](...layer.getArgs());
}
});
application().getApp().use(rawRouter.routes()).use(rawRouter.allowedMethods());
}
}
import {PlatformAdapter} from "@tsed/platform-http";
class PlatformFastify extends PlatformAdapter {
mapLayers(layers: PlatformLayer[]) {
const {app} = this;
const rawApp: FastifyInstance = app.getApp();
layers.forEach((layer) => {
switch (layer.method) {
case "use":
if ((rawApp as any).use) {
(rawApp as any).use(...layer.getArgs());
}
return;
case "statics":
this.statics(layer.path as string, layer.opts as any);
// rawApp.register();
return;
}
try {
rawApp.route({
method: layer.method.toUpperCase() as any,
url: layer.path as any,
handler: this.compose(layer), // this method emulate middleware on the route like is used in Express
config: {
rawBody: layer.handlers.some((handler) => handler.opts?.paramsTypes?.RAW_BODY)
}
});
} catch (er) {
logger().warn({
error_name: er.code,
error_message: er.message
});
}
});
}
}
bodyParser()
Configures body parsing middleware:
class PlatformYourFramework extends PlatformAdapter {
bodyParser(type: "json" | "text" | "urlencoded", additionalOptions: any = {}): any {
// Return appropriate body parser middleware
// ...
}
}
statics()
Configures static file serving:
class PlatformYourFramework extends PlatformAdapter {
statics(endpoint: string, options: PlatformStaticsOptions) {
// Return middleware for serving static files
// ...
}
}
4. Implement Response and Request Classes
Create custom implementations for PlatformResponse
and PlatformRequest
:
// PlatformYourFrameworkResponse.ts
import {PlatformResponse} from "@tsed/platform-http";
export class PlatformYourFrameworkResponse extends PlatformResponse {
// Implement methods to adapt your framework's response object
// ...
}
// PlatformYourFrameworkRequest.ts
import {PlatformRequest} from "@tsed/platform-http";
export class PlatformYourFrameworkRequest extends PlatformRequest {
// Implement methods to adapt your framework's request object
// ...
}
5. Add Bootstrap Methods
Add static methods to create and bootstrap applications:
class PlatformYourFramework extends PlatformAdapter {
static create(module: Type<any>, settings: Partial<TsED.Configuration> = {}) {
return PlatformBuilder.create<YourFramework.Application>(module, {
...settings,
adapter: PlatformYourFramework
});
}
static bootstrap(module: Type<any>, settings: Partial<TsED.Configuration> = {}) {
return PlatformBuilder.bootstrap<YourFramework.Application>(module, {
...settings,
adapter: PlatformYourFramework
});
}
}
Best Practices
Framework Specifics: Understand the specifics of your target framework, especially its middleware system and request/response lifecycle.
Error Handling: Implement proper error handling in your adapter to ensure exceptions are caught and processed correctly.
Context Management: Ensure that the Ts.ED context is properly created, stored, and cleaned up for each request.
Testing: Create comprehensive tests for your adapter to ensure it works correctly with the Ts.ED framework.
Documentation: Document any framework-specific behaviors or limitations of your adapter.
Testing Your Adapter
Ts.ED provides the @tsed/platform-test-sdk
package to help you test your custom platform adapter. This package includes a comprehensive test suite that verifies your adapter's compatibility with the Ts.ED framework.
It is strongly recommended to set up these tests early in your development process to ensure your adapter meets all the framework requirements.
Setting Up Tests
First, install the package:
npm install --save-dev @tsed/platform-test-sdk
Then create a test file for your adapter:
// platform-yourframework.spec.ts
import {PlatformTestSdk} from "@tsed/platform-test-sdk";
import {PlatformYourFramework} from "../src/components/PlatformYourFramework";
import {rootDir, Server} from "./app/Server";
const utils = PlatformTestSdk.create({
rootDir,
adapter: PlatformYourFramework,
server: Server,
logger: {
level: "off"
}
});
describe("PlatformYourFramework", () => {
describe("Handlers", () => {
utils.test("handlers");
});
describe("Response", () => {
utils.test("response");
});
describe("Middlewares", () => {
utils.test("middlewares");
});
describe("Headers", () => {
utils.test("headers");
});
describe("BodyParams", () => {
utils.test("bodyParams");
});
// Add more tests as needed
});
The PlatformTestSdk
will run a series of tests against your adapter to ensure it correctly implements all the required functionality.
What Gets Tested
The test suite covers various aspects of the platform adapter:
- Request and response handling
- Middleware execution
- Parameter binding (path, query, body, headers)
- Error handling
- Routing
- Content negotiation
- File uploads
- And more
Each test verifies that your adapter correctly implements the expected behavior for a specific feature of the Ts.ED framework.
Example Test Server
You'll need to create a test server for your tests:
// app/Server.ts
import {Configuration} from "@tsed/di";
import {PlatformYourFramework} from "../../src/components/PlatformYourFramework";
import path from "path";
export const rootDir = path.join(__dirname, "..");
@Configuration({
rootDir,
mount: {
"/rest": [`${rootDir}/controllers/**/*.ts`]
},
logger: {
level: "off"
}
})
export class Server {}
Integration Tests
Looking at the existing adapters, you can see how they implement their test suites:
// platform-express.spec.ts
import {PlatformTestSdk} from "@tsed/platform-test-sdk";
import {PlatformExpress} from "../src/components/PlatformExpress";
import {rootDir, Server} from "./app/Server";
const utils = PlatformTestSdk.create({
rootDir,
adapter: PlatformExpress,
server: Server,
logger: {
level: "off"
}
});
describe("PlatformExpress", () => {
describe("Handlers", () => {
utils.test("handlers");
});
// More tests...
});
// platform-koa.spec.ts
import {PlatformTestSdk} from "@tsed/platform-test-sdk";
import {PlatformKoa} from "../src/components/PlatformKoa";
import {rootDir, Server} from "./app/Server";
const utils = PlatformTestSdk.create({
rootDir,
adapter: PlatformKoa,
server: Server,
logger: {
level: "off"
}
});
describe("PlatformKoa", () => {
describe("Handlers", () => {
utils.test("handlers");
});
// More tests...
});
// platform-fastify.spec.ts
import {PlatformTestSdk} from "@tsed/platform-test-sdk";
import {PlatformFastify} from "../src/components/PlatformFastify";
import {rootDir, Server} from "./app/Server";
const utils = PlatformTestSdk.create({
rootDir,
adapter: PlatformFastify,
server: Server,
logger: {
level: "off"
}
});
describe("PlatformFastify", () => {
describe("Handlers", () => {
utils.test("handlers");
});
// More tests...
});
By implementing these tests early in your development process, you can ensure that your adapter correctly implements all the required functionality and meets the expectations of the Ts.ED framework.
Conclusion
Creating a custom platform adapter allows you to integrate any web framework with Ts.ED. By implementing the required methods and following the patterns established by the built-in adapters, you can create a seamless integration between your chosen framework and the Ts.ED ecosystem.
For more details, examine the source code of the existing adapters: