feat: integrate Hono with NestJS and add logging middleware

This commit is contained in:
2026-01-11 00:38:29 +07:00
parent e1e1d9cb7b
commit f80ea881c6
10 changed files with 732 additions and 63 deletions

152
build.ts Normal file
View File

@@ -0,0 +1,152 @@
#!/usr/bin/env bun
import { existsSync } from "fs";
import { rm } from "fs/promises";
import path from "path";
if (process.argv.includes("--help") || process.argv.includes("-h")) {
console.log(`
🏗️ Bun Build Script
Usage: bun run build.ts [options]
Common Options:
--outdir <path> Output directory (default: "dist")
--minify Enable minification (or --minify.whitespace, --minify.syntax, etc)
--sourcemap <type> Sourcemap type: none|linked|inline|external
--target <target> Build target: browser|bun|node
--format <format> Output format: esm|cjs|iife
--splitting Enable code splitting
--packages <type> Package handling: bundle|external
--public-path <path> Public path for assets
--env <mode> Environment handling: inline|disable|prefix*
--conditions <list> Package.json export conditions (comma separated)
--external <list> External packages (comma separated)
--banner <text> Add banner text to output
--footer <text> Add footer text to output
--define <obj> Define global constants (e.g. --define.VERSION=1.0.0)
--help, -h Show this help message
Example:
bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom
`);
process.exit(0);
}
const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, g => g[1].toUpperCase());
const parseValue = (value: string): any => {
if (value === "true") return true;
if (value === "false") return false;
if (/^\d+$/.test(value)) return parseInt(value, 10);
if (/^\d*\.\d+$/.test(value)) return parseFloat(value);
if (value.includes(",")) return value.split(",").map(v => v.trim());
return value;
};
function parseArgs(): Partial<Bun.BuildConfig> & { [key: string]: any } {
const config: Partial<Bun.BuildConfig> & { [key: string]: any } = {};
const args = process.argv.slice(2);
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === undefined) continue;
if (!arg.startsWith("--")) continue;
if (arg.startsWith("--no-")) {
const key = toCamelCase(arg.slice(5));
config[key] = false;
continue;
}
if (!arg.includes("=") && (i === args.length - 1 || args[i + 1]?.startsWith("--"))) {
const key = toCamelCase(arg.slice(2));
config[key] = true;
continue;
}
let key: string;
let value: string;
if (arg.includes("=")) {
[key, value] = arg.slice(2).split("=", 2) as [string, string];
} else {
key = arg.slice(2);
value = args[++i] ?? "";
}
key = toCamelCase(key);
if (key.includes(".")) {
const [parentKey, childKey] = key.split(".");
config[parentKey] = config[parentKey] || {};
config[parentKey][childKey] = parseValue(value);
} else {
config[key] = parseValue(value);
}
}
return config;
}
const formatFileSize = (bytes: number): string => {
const units = ["B", "KB", "MB", "GB"];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
};
console.log("\n🚀 Starting build process...\n");
const cliConfig = parseArgs();
const outdir = cliConfig.outdir || path.join(process.cwd(), "dist");
// if (existsSync(outdir)) {
// console.log(`🗑️ Cleaning previous build at ${outdir}`);
// await rm(outdir, { recursive: true, force: true });
// }
const start = performance.now();
// const entrypoints = [...new Bun.Glob("**.html").scanSync("src")]
// .map(a => path.resolve("src", a))
// .filter(dir => !dir.includes("node_modules"));
const entrypoints = [path.resolve("dist/server/index.js")];
console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`);
const result = await Bun.build({
entrypoints,
outdir,
plugins: [],
minify: false,
// target: "browser",
target: "bun",
// sourcemap: "linked",
sourcemap: false,
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
external: ["@nestjs/microservices",'@nestjs/platform-express', "@nestjs/websockets", "class-transformer", "class-validator"],
...cliConfig,
});
const end = performance.now();
const outputTable = result.outputs.map(output => ({
File: path.relative(process.cwd(), output.path),
Type: output.kind,
Size: formatFileSize(output.size),
}));
console.table(outputTable);
const buildTime = (end - start).toFixed(2);
console.log(`\n✅ Build completed in ${buildTime}ms\n`);

View File

@@ -33,6 +33,7 @@
"devDependencies": {
"@hattip/adapter-node": "^0.0.49",
"@hono-di/vite": "^0.0.15",
"@hono/node-server": "^1.19.8",
"@primevue/auto-import-resolver": "^4.5.4",
"@types/bun": "^1.3.5",
"@types/node": "^25.0.5",
@@ -267,6 +268,8 @@
"@hono-di/vite": ["@hono-di/vite@0.0.15", "", { "dependencies": { "@hono-di/client": "0.0.15", "@hono-di/generate": "0.0.15", "sirv": "^2.0.4" }, "peerDependencies": { "vite": "^7.0.0" } }, "sha512-VzqcqkReVHWxMiGHdnze1I9efNZTSMBmm2qRsLU7XDtJGBBPu3WzlNvlf84NjkExeYb55dMhDc3Cln7bKhv/LQ=="],
"@hono/node-server": ["@hono/node-server@1.19.8", "", { "peerDependencies": { "hono": "^4" } }, "sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA=="],
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
"@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="],

View File

@@ -3,7 +3,7 @@
"type": "module",
"scripts": {
"dev": "bunx --bun vite",
"build": "bunx --bun vite build && bun build dist/server/index.js --target bun --minify --outdir dist",
"build": "bunx --bun vite build && bun run build.ts",
"preview": "bunx --bun vite preview"
},
"dependencies": {
@@ -35,6 +35,7 @@
"devDependencies": {
"@hattip/adapter-node": "^0.0.49",
"@hono-di/vite": "^0.0.15",
"@hono/node-server": "^1.19.8",
"@primevue/auto-import-resolver": "^4.5.4",
"@types/bun": "^1.3.5",
"@types/node": "^25.0.5",

View File

@@ -64,7 +64,6 @@ export default function transformerClassnamesMinifier(options: CompileClassOptio
enforce: 'pre',
async transform(s, _id, { uno }) {
if(s.original.includes('p-button') || s.original.includes('p-component') || s.original.includes('p-button-secondary')) {
console.log("transforming:", _id);
}
const matches = [...s.original.matchAll(regexp)]
if (!matches.length)

View File

@@ -33,7 +33,7 @@ import { NestHonoApplication } from './server/HonoAdapter/interfaces';
// app.get("/.well-known/*", (c) => {
// return c.json({ ok: true });
// });
const app = await NestFactory.create<NestHonoApplication>(
const appHonoNest = await NestFactory.create<NestHonoApplication>(
AppModule,
new HonoAdapter(),
{
@@ -51,10 +51,26 @@ const app = await NestFactory.create<NestHonoApplication>(
// next();
// }
// });
const honoInstance = app.getHonoInstance();
honoInstance.use(contextStorage());
honoInstance.get("*", ssrRender);
// app.use('*', contextStorage());
await app.init();
const httpAdapter = app.get(HttpAdapterHost);
export default app
const app = appHonoNest.getHonoInstance();
app.use(async (c, next) => {
c.set("fetch", app.request.bind(app));
const ua = c.req.header("User-Agent")
if (!ua) {
return c.json({ error: "User-Agent header is missing" }, 400);
};
c.set("isMobile", isMobile({ ua }));
await next();
}, contextStorage());
appHonoNest.useStaticAssets("/*", { root: "./dist/client" });
await appHonoNest.init();
app.get("/.well-known/*", (c) => {
return c.json({ ok: true });
});
app.use(ssrRender);
const httpAdapter = appHonoNest.get(HttpAdapterHost);
// await app.listen(3000)
// console.log("HTTP Adapter:", app.fetch.toString());
export default {
fetch: app.fetch.bind(app),
}

View File

@@ -1,18 +1,6 @@
// @ts-ignore
// @ts-nocheck
import { HttpBindings, createAdaptorServer } from '@hono/node-server';
import {
ServeStaticOptions,
serveStatic,
} from '@hono/node-server/serve-static';
import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response';
import { RequestMethod } from '@nestjs/common';
import { HttpStatus, Logger } from '@nestjs/common';
import {
ErrorHandler,
NestApplicationOptions,
RequestHandler,
} from '@nestjs/common/interfaces';
import { ErrorHandler, NestApplicationOptions, RequestHandler } from '@nestjs/common/interfaces';
import { isObject } from '@nestjs/common/utils/shared.utils';
import { AbstractHttpAdapter } from '@nestjs/core/adapters/http-adapter';
import { Context, Next, Hono } from 'hono';
@@ -20,15 +8,14 @@ import { bodyLimit } from 'hono/body-limit';
import { cors } from 'hono/cors';
// import { Data } from 'hono/dist/types/context';
import { RedirectStatusCode, StatusCode } from 'hono/utils/http-status';
import * as http from 'http';
import http2 from 'http2';
import * as https from 'https';
import { HonoRequest, TypeBodyParser } from './interfaces';
import { Data } from 'node_modules/hono/dist/types/context';
// Define HttpBindings as an empty interface if you don't need specific bindings
interface HttpBindings {}
type HonoHandler = RequestHandler<HonoRequest, Context>;
type ServerType = http.Server | http2.Http2Server | http2.Http2SecureServer;
type Ctx = Context | (() => Promise<Context>);
type Method =
| 'all'
@@ -44,11 +31,11 @@ type Method =
* Adapter for using Hono with NestJS.
*/
export class HonoAdapter extends AbstractHttpAdapter<
ServerType,
any,
HonoRequest,
Context
> {
private _isParserRegistered: boolean;
private _isParserRegistered: boolean = false;
protected readonly instance: Hono<{ Bindings: HttpBindings }>;
@@ -74,7 +61,9 @@ export class HonoAdapter extends AbstractHttpAdapter<
private createRouteHandler(routeHandler: HonoHandler) {
return async (ctx: Context, next: Next) => {
ctx.req['params'] = ctx.req.param();
// ctx.req['params'] = ctx.req.param();
// ctx.req['params'] = ctx.req.param();
Object.assign(ctx.req, { params: ctx.req.param() });
await routeHandler(ctx.req, ctx, next);
@@ -110,7 +99,7 @@ export class HonoAdapter extends AbstractHttpAdapter<
responseContentType = 'application/json';
}
await this.setHeader(ctx, 'Content-Type', responseContentType);
await this.setHeader(ctx, 'Content-Type', String(responseContentType));
}
if (responseContentType === 'application/json' && isObject(body)) {
@@ -210,7 +199,10 @@ export class HonoAdapter extends AbstractHttpAdapter<
}
public async end() {
return RESPONSE_ALREADY_SENT;
// return RESPONSE_ALREADY_SENT;
new Response(null, {
headers: { ['x-hono-already-sent']: "true" }
})
}
public render() {
@@ -239,9 +231,14 @@ export class HonoAdapter extends AbstractHttpAdapter<
});
}
public useStaticAssets(path: string, options: ServeStaticOptions) {
public async useStaticAssets(path: string, options: any) {
Logger.log('Registering static assets middleware');
this.instance.use(path, serveStatic(options));
if ((process as any).versions?.bun && import.meta.env.PROD) {
const { serveStatic } = await import("hono/bun");
// app.use(serveStatic({ root: "./dist/client" }))
// this.instance.use(serveStatic({ root: "./dist/client" }))
this.instance.use(path, serveStatic(options));
}
}
public setViewEngine() {
@@ -303,7 +300,9 @@ export class HonoAdapter extends AbstractHttpAdapter<
Logger.log(
`Registering body parser middleware for type: ${type} | bodyLimit: ${bodyLimit}`,
);
this.instance.use(this.bodyLimit(bodyLimit));
if(bodyLimit !== undefined) {
this.instance.use(this.bodyLimit(bodyLimit));
}
// To avoid the Nest application init to override our custom
// body parser, we mark the parsers as registered.
@@ -325,7 +324,8 @@ export class HonoAdapter extends AbstractHttpAdapter<
ctx.req.header('x-cluster-client-ip') ??
ctx.req.header('x-forwarded') ??
ctx.req.header('forwarded-for') ??
ctx.req.header('via')
ctx.req.header('via')??
"unknown"
);
}
@@ -358,31 +358,28 @@ export class HonoAdapter extends AbstractHttpAdapter<
}
public initHttpServer(options: NestApplicationOptions) {
Logger.log('Initializing Hono HTTP server adapter');
this.instance.use(async (ctx, next) => {
ctx.req['ip'] = this.extractClientIp(ctx);
// ctx.req['ip'] = this.extractClientIp(ctx);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ctx.req['query'] = ctx.req.query() as any;
ctx.req['headers'] = Object.fromEntries(ctx.req.raw.headers);
// ctx.req['query'] = ctx.req.query() as any;
// ctx.req['headers'] = Object.fromEntries(ctx.req.raw.headers);
Object.assign(ctx.req, {
ip: this.extractClientIp(ctx),
query: ctx.req.query() as Record<string, string>,
headers: Object.fromEntries(ctx.req.raw.headers as any),
})
const contentType = ctx.req.header('content-type');
await this.parseRequestBody(ctx, contentType, options.rawBody);
if (contentType) {
await this.parseRequestBody(ctx, contentType, options.rawBody!);
}
await next();
});
const isHttpsEnabled = options?.httpsOptions;
const createServer = isHttpsEnabled
? https.createServer
: http.createServer;
this.httpServer = createAdaptorServer({
fetch: this.instance.fetch,
createServer,
overrideGlobalObjects: false,
});
return this.httpServer;
}
public getType(): string {
return 'hono';
}
public getType(): string { return 'hono'; }
public registerParserMiddleware(_prefix?: string, rawBody?: boolean) {
if (this._isParserRegistered) {
@@ -400,7 +397,7 @@ export class HonoAdapter extends AbstractHttpAdapter<
public async createMiddlewareFactory(requestMethod: RequestMethod) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
return (path: string, callback: Function) => {
const routeMethodsMap = {
const routeMethodsMap: Record<string, Function> = {
[RequestMethod.ALL]: this.instance.all,
[RequestMethod.DELETE]: this.instance.delete,
[RequestMethod.GET]: this.instance.get,
@@ -424,12 +421,7 @@ export class HonoAdapter extends AbstractHttpAdapter<
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public listen(port: string | number, ...args: any[]): ServerType {
return this.httpServer.listen(port, ...args);
}
public fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
return this.instance.fetch(input, init);
}
public listen(...args: any[]) {}
public getHonoInstance(): Hono<{ Bindings: HttpBindings }> {
return this.instance;
}

View File

@@ -73,7 +73,7 @@ export interface NestHonoApplication<
callback?: (err: Error, address: string) => void,
): Promise<TServer>;
fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
getHonoInstance(): Hono<{ Bindings: any }>;
getHonoInstance(): Hono;
}
export type HonoRequest = Request & {
headers?: Record<string, string>;

View File

@@ -0,0 +1,466 @@
// import { HttpBindings, createAdaptorServer } from '@hono/node-server';
// import { ServeStaticOptions, serveStatic } from '@hono/node-server/serve-static';
// import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response';
import { RequestMethod } from '@nestjs/common';
import { HttpStatus, Logger } from '@nestjs/common';
import {
ErrorHandler,
NestApplicationOptions,
RequestHandler,
} from '@nestjs/common/interfaces';
import { isObject } from '@nestjs/common/utils/shared.utils';
import { AbstractHttpAdapter } from '@nestjs/core/adapters/http-adapter';
import { Context, Next, Hono } from 'hono';
import { bodyLimit } from 'hono/body-limit';
import { cors } from 'hono/cors';
// import { Data } from 'hono/dist/types/context';
import { RedirectStatusCode, StatusCode } from 'hono/utils/http-status';
import http2 from 'http2';
import { createServer as createServerHTTP, Server } from 'http';
import { createServer as createServerHTTPS } from 'https';
import { HonoRequest, TypeBodyParser } from './interfaces';
import { Data } from 'node_modules/hono/dist/types/context';
// Define HttpBindings as an empty interface if you don't need specific bindings
interface HttpBindings {}
type HonoHandler = RequestHandler<HonoRequest, Context>;
type ServerType = Server | http2.Http2Server | http2.Http2SecureServer;
type Ctx = Context | (() => Promise<Context>);
type Method =
| 'all'
| 'get'
| 'post'
| 'put'
| 'delete'
| 'use'
| 'patch'
| 'options';
/**
* Adapter for using Hono with NestJS.
*/
export class HonoAdapter extends AbstractHttpAdapter<
ServerType,
HonoRequest,
Context
> {
private _isParserRegistered: boolean = false;
protected readonly instance: Hono<{ Bindings: HttpBindings }>;
constructor() {
const honoInstance = new Hono<{ Bindings: HttpBindings }>();
super(honoInstance);
this.instance = honoInstance;
}
get isParserRegistered(): boolean {
return !!this._isParserRegistered;
}
private getRouteAndHandler(
pathOrHandler: string | HonoHandler,
handler?: HonoHandler,
): [string, HonoHandler] {
const path = typeof pathOrHandler === 'function' ? '' : pathOrHandler;
handler = typeof pathOrHandler === 'function' ? pathOrHandler : handler;
return [path, handler!];
}
private createRouteHandler(routeHandler: HonoHandler) {
return async (ctx: Context, next: Next) => {
// ctx.req['params'] = ctx.req.param();
// ctx.req['params'] = ctx.req.param();
Object.assign(ctx.req, { params: ctx.req.param() });
await routeHandler(ctx.req, ctx, next);
return this.getBody(ctx);
};
}
private async normalizeContext(ctx: Ctx): Promise<Context> {
if (typeof ctx === 'function') {
return await ctx();
}
return ctx;
}
private async getBody(ctx: Ctx, body?: Data) {
ctx = await this.normalizeContext(ctx);
if (body === undefined && ctx.res && ctx.res.body !== null) {
return ctx.res;
}
let responseContentType = await this.getHeader(ctx, 'Content-Type');
if (!responseContentType || responseContentType.startsWith('text/plain')) {
if (
body instanceof Buffer ||
body instanceof Uint8Array ||
body instanceof ArrayBuffer ||
body instanceof ReadableStream
) {
responseContentType = 'application/octet-stream';
} else if (isObject(body)) {
responseContentType = 'application/json';
}
await this.setHeader(ctx, 'Content-Type', String(responseContentType));
}
if (responseContentType === 'application/json' && isObject(body)) {
return ctx.json(body);
} else if (body === undefined) {
return ctx.newResponse(null);
}
return ctx.body(body);
}
private registerRoute(
method: Method,
pathOrHandler: string | HonoHandler,
handler?: HonoHandler,
) {
const [routePath, routeHandler] = this.getRouteAndHandler(
pathOrHandler,
handler,
);
const routeHandler2 = this.createRouteHandler(routeHandler);
switch (method) {
case 'all':
this.instance.all(routePath, routeHandler2);
break;
case 'get':
this.instance.get(routePath, routeHandler2);
break;
case 'post':
this.instance.post(routePath, routeHandler2);
break;
case 'put':
this.instance.put(routePath, routeHandler2);
break;
case 'delete':
this.instance.delete(routePath, routeHandler2);
break;
case 'use':
this.instance.use(routePath, routeHandler2);
break;
case 'patch':
this.instance.patch(routePath, routeHandler2);
break;
case 'options':
this.instance.options(routePath, routeHandler2);
break;
}
}
public all(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('all', pathOrHandler, handler);
}
public get(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('get', pathOrHandler, handler);
}
public post(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('post', pathOrHandler, handler);
}
public put(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('put', pathOrHandler, handler);
}
public delete(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('delete', pathOrHandler, handler);
}
public use(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('use', pathOrHandler, handler);
}
public patch(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('patch', pathOrHandler, handler);
}
public options(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('options', pathOrHandler, handler);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public async reply(ctx: Ctx, body: any, statusCode?: StatusCode) {
ctx = await this.normalizeContext(ctx);
if (statusCode) {
ctx.status(statusCode);
}
ctx.res = await this.getBody(ctx, body);
}
public async status(ctx: Ctx, statusCode: StatusCode) {
ctx = await this.normalizeContext(ctx);
return ctx.status(statusCode);
}
public async end() {
// return RESPONSE_ALREADY_SENT;
new Response(null, {
headers: { ['x-hono-already-sent']: "true" }
})
}
public render() {
throw new Error('Method not implemented.');
}
public async redirect(ctx: Ctx, statusCode: RedirectStatusCode, url: string) {
ctx = await this.normalizeContext(ctx);
ctx.res = ctx.redirect(url, statusCode);
}
public setErrorHandler(handler: ErrorHandler) {
this.instance.onError(async (err: Error, ctx: Context) => {
await handler(err, ctx.req, ctx);
return this.getBody(ctx);
});
}
public setNotFoundHandler(handler: RequestHandler) {
this.instance.notFound(async (ctx: Context) => {
await handler(ctx.req, ctx);
await this.status(ctx, HttpStatus.NOT_FOUND);
return this.getBody(ctx, 'Not Found');
});
}
public async useStaticAssets(path: string, options: any) {
Logger.log("Đã bị comment trong adapter")
Logger.log('Registering static assets middleware');
if ((process as any).versions?.bun) {
const { serveStatic } = await import("hono/bun");
// app.use(serveStatic({ root: "./dist/client" }))
// this.instance.use(serveStatic({ root: "./dist/client" }))
this.instance.use(path, serveStatic(options));
}
}
public setViewEngine() {
throw new Error('Method not implemented.');
}
public async isHeadersSent(ctx: Ctx): Promise<boolean> {
ctx = await this.normalizeContext(ctx);
return ctx.finalized;
}
public async getHeader(ctx: Ctx, name: string) {
ctx = await this.normalizeContext(ctx);
return ctx.res.headers.get(name);
}
public async setHeader(ctx: Ctx, name: string, value: string) {
ctx = await this.normalizeContext(ctx);
ctx.res.headers.set(name, value);
}
public async appendHeader(ctx: Ctx, name: string, value: string) {
ctx = await this.normalizeContext(ctx);
ctx.res.headers.append(name, value);
}
public async getRequestHostname(ctx: Ctx): Promise<string> {
ctx = await this.normalizeContext(ctx);
return ctx.req.header().host;
}
public getRequestMethod(request: HonoRequest): string {
return request.method;
}
public getRequestUrl(request: HonoRequest): string {
return request.url;
}
public enableCors(options: {
origin:
| string
| string[]
| ((origin: string, c: Context) => string | undefined | null);
allowMethods?: string[];
allowHeaders?: string[];
maxAge?: number;
credentials?: boolean;
exposeHeaders?: string[];
}) {
this.instance.use(cors(options));
}
public useBodyParser(
type: TypeBodyParser,
rawBody?: boolean,
bodyLimit?: number,
) {
Logger.log(
`Registering body parser middleware for type: ${type} | bodyLimit: ${bodyLimit}`,
);
if(bodyLimit !== undefined) {
this.instance.use(this.bodyLimit(bodyLimit));
}
// To avoid the Nest application init to override our custom
// body parser, we mark the parsers as registered.
this._isParserRegistered = true;
}
public close(): Promise<void> {
return new Promise((resolve) => this.httpServer.close(() => resolve()));
}
private extractClientIp(ctx: Context): string {
return (
ctx.req.header('cf-connecting-ip') ??
ctx.req.header('x-forwarded-for') ??
ctx.req.header('x-real-ip') ??
ctx.req.header('forwarded') ??
ctx.req.header('true-client-ip') ??
ctx.req.header('x-client-ip') ??
ctx.req.header('x-cluster-client-ip') ??
ctx.req.header('x-forwarded') ??
ctx.req.header('forwarded-for') ??
ctx.req.header('via')??
"unknown"
);
}
private async parseRequestBody(
ctx: Context,
contentType: string,
rawBody: boolean,
): Promise<void> {
if (
contentType?.startsWith('multipart/form-data') ||
contentType?.startsWith('application/x-www-form-urlencoded')
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(ctx.req as any).body = await ctx.req
.parseBody({
all: true,
})
.catch(() => {});
} else if (
contentType?.startsWith('application/json') ||
contentType?.startsWith('text/plain')
) {
if (rawBody) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(ctx.req as any).rawBody = Buffer.from(await ctx.req.text());
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(ctx.req as any).body = await ctx.req.json().catch(() => {});
}
}
public initHttpServer(options: NestApplicationOptions) {
Logger.log('Initializing Hono HTTP server adapter');
this.instance.use(async (ctx, next) => {
// ctx.req['ip'] = this.extractClientIp(ctx);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// ctx.req['query'] = ctx.req.query() as any;
// ctx.req['headers'] = Object.fromEntries(ctx.req.raw.headers);
Object.assign(ctx.req, {
ip: this.extractClientIp(ctx),
query: ctx.req.query() as Record<string, string>,
headers: Object.fromEntries(ctx.req.raw.headers as any),
})
const contentType = ctx.req.header('content-type');
if (contentType) {
await this.parseRequestBody(ctx, contentType, options.rawBody!);
}
await next();
});
const isHttpsEnabled = options?.httpsOptions;
const createServer = isHttpsEnabled
? createServerHTTPS
: createServerHTTP;
// this.httpServer = createAdaptorServer({
// fetch: this.instance.fetch,
// createServer,
// overrideGlobalObjects: false,
// });
// this.httpServer = isHttpsEnabled ?
return this.httpServer;
}
public getType(): string {
return 'hono';
}
public registerParserMiddleware(_prefix?: string, rawBody?: boolean) {
if (this._isParserRegistered) {
return;
}
Logger.log('Registering parser middleware');
this.useBodyParser('application/x-www-form-urlencoded', rawBody);
this.useBodyParser('application/json', rawBody);
this.useBodyParser('text/plain', rawBody);
this._isParserRegistered = true;
}
public async createMiddlewareFactory(requestMethod: RequestMethod) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
return (path: string, callback: Function) => {
const routeMethodsMap: Record<string, Function> = {
[RequestMethod.ALL]: this.instance.all,
[RequestMethod.DELETE]: this.instance.delete,
[RequestMethod.GET]: this.instance.get,
[RequestMethod.OPTIONS]: this.instance.options,
[RequestMethod.PATCH]: this.instance.patch,
[RequestMethod.POST]: this.instance.post,
[RequestMethod.PUT]: this.instance.put,
};
const routeMethod = (
routeMethodsMap[requestMethod] || this.instance.get
).bind(this.instance);
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
routeMethod(path, async (ctx: Context, next: Function) => {
await callback(ctx.req, ctx, next);
});
};
}
public applyVersionFilter(): () => () => unknown {
throw new Error('Versioning not yet supported in Hono');
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public listen(port: string | number, ...args: any[]): ServerType {
return this.httpServer.listen(port, ...args);
}
public fetch(input: RequestInfo, init?: RequestInit): Response | Promise<Response> {
return this.instance.fetch(input as Request, init);
}
public getHonoInstance(): Hono<{ Bindings: HttpBindings }> {
return this.instance;
}
public bodyLimit(maxSize: number) {
return bodyLimit({
maxSize,
onError: (ctx) => {
const errorMessage = `Body size exceeded: ${maxSize} bytes. Size: ${ctx.req.header('Content-Length')} bytes. Method: ${ctx.req.method}. Path: ${ctx.req.path}`;
throw new Error(errorMessage);
},
});
}
}

View File

@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { LoggerMiddleware } from './middleware';
@Module({
imports: [
@@ -13,4 +14,9 @@ import { AppService } from './app.service';
AppService, // hono-di:providers
],
})
export class AppModule {}
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware).forRoutes('*');
}
}

34
src/server/middleware.ts Normal file
View File

@@ -0,0 +1,34 @@
// Source - https://stackoverflow.com/a
// Posted by Stark Jeon
// Retrieved 2026-01-10, License - CC BY-SA 4.0
import { Injectable, NestMiddleware, Logger } from "@nestjs/common";
import { Context } from "hono";
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
private logger = new Logger("HTTP");
async use(request: Request, response: Response, next: any) {
const start = Date.now()
// console.log("Request:", request.method, request.url, arguments[2].toString());
// const { ip, method, originalUrl } = request;
// const userAgent = request.get("user-agent") || "";
const ctx = arguments[2] as Context;
// ctx
// response.on("finish", () => {
// const { statusCode } = response;
// const contentLength = response.get("content-length");
// this.logger.log(
// `${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`,
// );
// });
await next().finally(() => {
const ms = Date.now() - start
this.logger.log(
`${request.method} ${request.url} - ${ms}ms`,
) ;
});
}
}