@kizunajs/express 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.MD ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sondre Ørland
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @kizunajs/express
2
+
3
+ `@kizunajs/express` connects a Kizuna API to an Express 5 application. It handles routing, request validation, body parsing, and error formatting, all driven by what you declared.
4
+
5
+ **Requires Express >= 5.**
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pnpm add @kizunajs/express express
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ // kizuna.config.ts
17
+ import { defineConfig } from 'kizunajs';
18
+ import { expressAdapter } from '@kizunajs/express';
19
+ import { routes } from './src/routes';
20
+
21
+ export default defineConfig({
22
+ adapter: expressAdapter(),
23
+ routes,
24
+ });
25
+ ```
26
+
27
+ ```ts
28
+ // src/index.ts
29
+ import express from 'express';
30
+ import kizuna from '../kizuna.config';
31
+
32
+ const app = express();
33
+ app.use(express.json());
34
+
35
+ kizuna.api.mount(app);
36
+
37
+ app.listen(3000);
38
+ ```
39
+
40
+ ## Documentation
41
+
42
+ [Express adapter](https://kizunajs.com/docs/adapters/express)
package/dist/index.cjs ADDED
@@ -0,0 +1,156 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let express = require("express");
3
+ let node_stream = require("node:stream");
4
+ let kizunajs_adapter = require("kizunajs/adapter");
5
+ //#region src/server.ts
6
+ const writeStream = (open, res, validate) => {
7
+ const controller = new AbortController();
8
+ res.on("close", () => {
9
+ if (!res.writableFinished) controller.abort();
10
+ });
11
+ res.flushHeaders();
12
+ const stream = open({
13
+ signal: controller.signal,
14
+ validate
15
+ });
16
+ (0, node_stream.pipeline)(node_stream.Readable.fromWeb(stream), res, () => void 0);
17
+ };
18
+ /**
19
+ * Write a web `Response` to a node response. Plugins answer in web terms to stay
20
+ * adapter-agnostic, so the translation belongs here.
21
+ */
22
+ const writeWebResponse = async (response, res) => {
23
+ if (!(response instanceof globalThis.Response)) return;
24
+ res.status(response.status);
25
+ response.headers.forEach((value, name) => res.setHeader(name, value));
26
+ if (!response.body) {
27
+ res.end();
28
+ return;
29
+ }
30
+ node_stream.Readable.fromWeb(response.body).pipe(res);
31
+ };
32
+ const adapter = (0, kizunajs_adapter.createAdapter)({
33
+ buildHandlerContext: (adapterRequest, { res }) => ({
34
+ req: adapterRequest.request,
35
+ res
36
+ }),
37
+ respond: (result, { res, next, formatError, responseValidation }) => {
38
+ if (result.kind === "handler-error") {
39
+ next(result.error);
40
+ return;
41
+ }
42
+ if (result.kind === "raw-response") {
43
+ writeWebResponse(result.response, res);
44
+ return;
45
+ }
46
+ if (res.headersSent) return;
47
+ if (result.kind === "not-found" || result.kind === "method-not-allowed") {
48
+ next();
49
+ return;
50
+ }
51
+ const rendered = (0, kizunajs_adapter.renderJsonResult)(result, formatError, res.req);
52
+ for (const [key, value] of Object.entries(rendered.headers)) res.setHeader(key, value);
53
+ if (rendered.stream) {
54
+ res.status(rendered.status);
55
+ if (res.req.method === "HEAD") {
56
+ res.end();
57
+ return;
58
+ }
59
+ writeStream(rendered.stream, res, responseValidation);
60
+ return;
61
+ }
62
+ if (rendered.body === void 0) res.status(rendered.status).end();
63
+ else if (rendered.raw) {
64
+ const body = rendered.body;
65
+ res.status(rendered.status).send(typeof body === "string" || Buffer.isBuffer(body) ? body : Buffer.from(body));
66
+ } else res.status(rendered.status).json(rendered.body);
67
+ }
68
+ });
69
+ /**
70
+ * Mount a Kizuna API onto an Express app.
71
+ *
72
+ * @example
73
+ * api.mount(app);
74
+ */
75
+ function mountExpress(api, app, options) {
76
+ const guards = api[kizunajs_adapter.GUARDS_META];
77
+ const schemes = api[kizunajs_adapter.SCHEMES_META];
78
+ const requestContext = api[kizunajs_adapter.REQUEST_CONTEXT_META];
79
+ const pluginExports = (0, kizunajs_adapter.pluginExportsOf)(api);
80
+ const jobsMeta = api[kizunajs_adapter.JOBS_META];
81
+ const jobRunner = (0, kizunajs_adapter.jobRunnerFrom)(jobsMeta);
82
+ const expressRouter = (0, express.Router)();
83
+ const mountRoute = (routeKey, route, routes, router) => {
84
+ expressRouter[route.method.toLowerCase()](route.path, (req, _res, next) => {
85
+ req.kizunaRoute = route;
86
+ next();
87
+ }, async (req, res, next) => {
88
+ const adapterRequest = {
89
+ request: req,
90
+ method: req.method,
91
+ resolution: {
92
+ kind: "pre-resolved",
93
+ routeKey,
94
+ route,
95
+ params: req.params
96
+ },
97
+ query: req.query,
98
+ headers: req.headers,
99
+ readBody: () => req.body
100
+ };
101
+ await adapter.handle({
102
+ routes,
103
+ router,
104
+ request: adapterRequest,
105
+ responseContext: {
106
+ res,
107
+ next,
108
+ formatError: options?.formatError,
109
+ responseValidation: options?.responseValidation
110
+ },
111
+ guards,
112
+ schemes,
113
+ requestContext,
114
+ pluginExports,
115
+ jobs: jobRunner,
116
+ responseValidation: options?.responseValidation
117
+ });
118
+ });
119
+ };
120
+ const mountLane = (routes, router) => {
121
+ const declaredRoutes = [...adapter.eachRoute(routes, router)].sort((left, right) => Number(right.route.method === "HEAD") - Number(left.route.method === "HEAD"));
122
+ for (const { routeKey, route } of declaredRoutes) mountRoute(routeKey, route, routes, router);
123
+ };
124
+ mountLane(api.routes, api[kizunajs_adapter.ROUTER_META]);
125
+ mountLane((0, kizunajs_adapter.pluginRoutesOf)(api), (0, kizunajs_adapter.pluginRouterOf)(api));
126
+ if (jobsMeta) {
127
+ const routes = (0, kizunajs_adapter.jobRoutes)(jobsMeta);
128
+ const router = (0, kizunajs_adapter.jobRouter)(jobsMeta);
129
+ for (const [routeKey, route] of Object.entries(routes)) mountRoute(routeKey, route, routes, router);
130
+ }
131
+ app.use(expressRouter);
132
+ return expressRouter;
133
+ }
134
+ /**
135
+ * Serve an API on Express. What it takes is what every route is served with.
136
+ *
137
+ * @example
138
+ * // kizuna.config.ts
139
+ * export default defineConfig({
140
+ * adapter: expressAdapter(),
141
+ * routes,
142
+ * });
143
+ *
144
+ * // src/index.ts
145
+ * kizuna.api.mount(app);
146
+ */
147
+ const expressAdapter = (defaults) => ({
148
+ name: "express",
149
+ mount: (api, app, options) => mountExpress(api, app, {
150
+ ...defaults,
151
+ ...options
152
+ })
153
+ });
154
+ //#endregion
155
+ exports.expressAdapter = expressAdapter;
156
+ exports.mountExpress = mountExpress;
@@ -0,0 +1,68 @@
1
+ import { Request, Response, Router } from "express";
2
+ import { Adapter, ApiWithRouter, ErrorFormatter, GUARDS_META, JOBS_META, REQUEST_CONTEXT_META, RouteDefinition, Routes, SCHEMES_META } from "kizunajs/adapter";
3
+
4
+ //#region src/server.d.ts
5
+ type ExpressApi<R extends Routes = Routes> = ApiWithRouter<R> & {
6
+ readonly [GUARDS_META]?: unknown;
7
+ readonly [SCHEMES_META]?: unknown;
8
+ readonly [REQUEST_CONTEXT_META]?: unknown;
9
+ readonly [JOBS_META]?: unknown;
10
+ /**
11
+ * Register every route on an Express app or router.
12
+ */
13
+ mount: (app: AppLike, options?: ExpressOptions) => Router;
14
+ };
15
+ /**
16
+ * The Express request and response passed to each handler.
17
+ */
18
+ interface ExpressHandlerContext {
19
+ req: Request;
20
+ res: Response;
21
+ }
22
+ declare global {
23
+ namespace Express {
24
+ interface Request {
25
+ kizunaRoute?: RouteDefinition;
26
+ }
27
+ }
28
+ }
29
+ interface ExpressOptions {
30
+ /**
31
+ * Validate handler return values against the route's response schemas.
32
+ * Mismatches surface as 500 errors. Enable in development.
33
+ *
34
+ * @default false
35
+ */
36
+ responseValidation?: boolean;
37
+ /**
38
+ * Reshape error (status >= 400) response bytes before they are sent. See
39
+ * {@link ErrorFormatter}.
40
+ */
41
+ formatError?: ErrorFormatter<Request>;
42
+ }
43
+ interface AppLike {
44
+ use: (router: Router) => unknown;
45
+ }
46
+ /**
47
+ * Mount a Kizuna API onto an Express app.
48
+ *
49
+ * @example
50
+ * api.mount(app);
51
+ */
52
+ declare function mountExpress(api: ExpressApi, app: AppLike, options?: ExpressOptions): Router;
53
+ /**
54
+ * Serve an API on Express. What it takes is what every route is served with.
55
+ *
56
+ * @example
57
+ * // kizuna.config.ts
58
+ * export default defineConfig({
59
+ * adapter: expressAdapter(),
60
+ * routes,
61
+ * });
62
+ *
63
+ * // src/index.ts
64
+ * kizuna.api.mount(app);
65
+ */
66
+ declare const expressAdapter: (defaults?: ExpressOptions) => Adapter<ExpressHandlerContext, [app: AppLike, options?: ExpressOptions], Router>;
67
+ //#endregion
68
+ export { AppLike, ExpressApi, ExpressHandlerContext, ExpressOptions, expressAdapter, mountExpress };
@@ -0,0 +1,68 @@
1
+ import { Request, Response, Router } from "express";
2
+ import { Adapter, ApiWithRouter, ErrorFormatter, GUARDS_META, JOBS_META, REQUEST_CONTEXT_META, RouteDefinition, Routes, SCHEMES_META } from "kizunajs/adapter";
3
+
4
+ //#region src/server.d.ts
5
+ type ExpressApi<R extends Routes = Routes> = ApiWithRouter<R> & {
6
+ readonly [GUARDS_META]?: unknown;
7
+ readonly [SCHEMES_META]?: unknown;
8
+ readonly [REQUEST_CONTEXT_META]?: unknown;
9
+ readonly [JOBS_META]?: unknown;
10
+ /**
11
+ * Register every route on an Express app or router.
12
+ */
13
+ mount: (app: AppLike, options?: ExpressOptions) => Router;
14
+ };
15
+ /**
16
+ * The Express request and response passed to each handler.
17
+ */
18
+ interface ExpressHandlerContext {
19
+ req: Request;
20
+ res: Response;
21
+ }
22
+ declare global {
23
+ namespace Express {
24
+ interface Request {
25
+ kizunaRoute?: RouteDefinition;
26
+ }
27
+ }
28
+ }
29
+ interface ExpressOptions {
30
+ /**
31
+ * Validate handler return values against the route's response schemas.
32
+ * Mismatches surface as 500 errors. Enable in development.
33
+ *
34
+ * @default false
35
+ */
36
+ responseValidation?: boolean;
37
+ /**
38
+ * Reshape error (status >= 400) response bytes before they are sent. See
39
+ * {@link ErrorFormatter}.
40
+ */
41
+ formatError?: ErrorFormatter<Request>;
42
+ }
43
+ interface AppLike {
44
+ use: (router: Router) => unknown;
45
+ }
46
+ /**
47
+ * Mount a Kizuna API onto an Express app.
48
+ *
49
+ * @example
50
+ * api.mount(app);
51
+ */
52
+ declare function mountExpress(api: ExpressApi, app: AppLike, options?: ExpressOptions): Router;
53
+ /**
54
+ * Serve an API on Express. What it takes is what every route is served with.
55
+ *
56
+ * @example
57
+ * // kizuna.config.ts
58
+ * export default defineConfig({
59
+ * adapter: expressAdapter(),
60
+ * routes,
61
+ * });
62
+ *
63
+ * // src/index.ts
64
+ * kizuna.api.mount(app);
65
+ */
66
+ declare const expressAdapter: (defaults?: ExpressOptions) => Adapter<ExpressHandlerContext, [app: AppLike, options?: ExpressOptions], Router>;
67
+ //#endregion
68
+ export { AppLike, ExpressApi, ExpressHandlerContext, ExpressOptions, expressAdapter, mountExpress };
package/dist/index.mjs ADDED
@@ -0,0 +1,154 @@
1
+ import { Router } from "express";
2
+ import { Readable, pipeline } from "node:stream";
3
+ import { GUARDS_META, JOBS_META, REQUEST_CONTEXT_META, ROUTER_META, SCHEMES_META, createAdapter, jobRouter, jobRoutes, jobRunnerFrom, pluginExportsOf, pluginRouterOf, pluginRoutesOf, renderJsonResult } from "kizunajs/adapter";
4
+ //#region src/server.ts
5
+ const writeStream = (open, res, validate) => {
6
+ const controller = new AbortController();
7
+ res.on("close", () => {
8
+ if (!res.writableFinished) controller.abort();
9
+ });
10
+ res.flushHeaders();
11
+ const stream = open({
12
+ signal: controller.signal,
13
+ validate
14
+ });
15
+ pipeline(Readable.fromWeb(stream), res, () => void 0);
16
+ };
17
+ /**
18
+ * Write a web `Response` to a node response. Plugins answer in web terms to stay
19
+ * adapter-agnostic, so the translation belongs here.
20
+ */
21
+ const writeWebResponse = async (response, res) => {
22
+ if (!(response instanceof globalThis.Response)) return;
23
+ res.status(response.status);
24
+ response.headers.forEach((value, name) => res.setHeader(name, value));
25
+ if (!response.body) {
26
+ res.end();
27
+ return;
28
+ }
29
+ Readable.fromWeb(response.body).pipe(res);
30
+ };
31
+ const adapter = createAdapter({
32
+ buildHandlerContext: (adapterRequest, { res }) => ({
33
+ req: adapterRequest.request,
34
+ res
35
+ }),
36
+ respond: (result, { res, next, formatError, responseValidation }) => {
37
+ if (result.kind === "handler-error") {
38
+ next(result.error);
39
+ return;
40
+ }
41
+ if (result.kind === "raw-response") {
42
+ writeWebResponse(result.response, res);
43
+ return;
44
+ }
45
+ if (res.headersSent) return;
46
+ if (result.kind === "not-found" || result.kind === "method-not-allowed") {
47
+ next();
48
+ return;
49
+ }
50
+ const rendered = renderJsonResult(result, formatError, res.req);
51
+ for (const [key, value] of Object.entries(rendered.headers)) res.setHeader(key, value);
52
+ if (rendered.stream) {
53
+ res.status(rendered.status);
54
+ if (res.req.method === "HEAD") {
55
+ res.end();
56
+ return;
57
+ }
58
+ writeStream(rendered.stream, res, responseValidation);
59
+ return;
60
+ }
61
+ if (rendered.body === void 0) res.status(rendered.status).end();
62
+ else if (rendered.raw) {
63
+ const body = rendered.body;
64
+ res.status(rendered.status).send(typeof body === "string" || Buffer.isBuffer(body) ? body : Buffer.from(body));
65
+ } else res.status(rendered.status).json(rendered.body);
66
+ }
67
+ });
68
+ /**
69
+ * Mount a Kizuna API onto an Express app.
70
+ *
71
+ * @example
72
+ * api.mount(app);
73
+ */
74
+ function mountExpress(api, app, options) {
75
+ const guards = api[GUARDS_META];
76
+ const schemes = api[SCHEMES_META];
77
+ const requestContext = api[REQUEST_CONTEXT_META];
78
+ const pluginExports = pluginExportsOf(api);
79
+ const jobsMeta = api[JOBS_META];
80
+ const jobRunner = jobRunnerFrom(jobsMeta);
81
+ const expressRouter = Router();
82
+ const mountRoute = (routeKey, route, routes, router) => {
83
+ expressRouter[route.method.toLowerCase()](route.path, (req, _res, next) => {
84
+ req.kizunaRoute = route;
85
+ next();
86
+ }, async (req, res, next) => {
87
+ const adapterRequest = {
88
+ request: req,
89
+ method: req.method,
90
+ resolution: {
91
+ kind: "pre-resolved",
92
+ routeKey,
93
+ route,
94
+ params: req.params
95
+ },
96
+ query: req.query,
97
+ headers: req.headers,
98
+ readBody: () => req.body
99
+ };
100
+ await adapter.handle({
101
+ routes,
102
+ router,
103
+ request: adapterRequest,
104
+ responseContext: {
105
+ res,
106
+ next,
107
+ formatError: options?.formatError,
108
+ responseValidation: options?.responseValidation
109
+ },
110
+ guards,
111
+ schemes,
112
+ requestContext,
113
+ pluginExports,
114
+ jobs: jobRunner,
115
+ responseValidation: options?.responseValidation
116
+ });
117
+ });
118
+ };
119
+ const mountLane = (routes, router) => {
120
+ const declaredRoutes = [...adapter.eachRoute(routes, router)].sort((left, right) => Number(right.route.method === "HEAD") - Number(left.route.method === "HEAD"));
121
+ for (const { routeKey, route } of declaredRoutes) mountRoute(routeKey, route, routes, router);
122
+ };
123
+ mountLane(api.routes, api[ROUTER_META]);
124
+ mountLane(pluginRoutesOf(api), pluginRouterOf(api));
125
+ if (jobsMeta) {
126
+ const routes = jobRoutes(jobsMeta);
127
+ const router = jobRouter(jobsMeta);
128
+ for (const [routeKey, route] of Object.entries(routes)) mountRoute(routeKey, route, routes, router);
129
+ }
130
+ app.use(expressRouter);
131
+ return expressRouter;
132
+ }
133
+ /**
134
+ * Serve an API on Express. What it takes is what every route is served with.
135
+ *
136
+ * @example
137
+ * // kizuna.config.ts
138
+ * export default defineConfig({
139
+ * adapter: expressAdapter(),
140
+ * routes,
141
+ * });
142
+ *
143
+ * // src/index.ts
144
+ * kizuna.api.mount(app);
145
+ */
146
+ const expressAdapter = (defaults) => ({
147
+ name: "express",
148
+ mount: (api, app, options) => mountExpress(api, app, {
149
+ ...defaults,
150
+ ...options
151
+ })
152
+ });
153
+ //#endregion
154
+ export { expressAdapter, mountExpress };
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@kizunajs/express",
3
+ "version": "2.0.0-beta.0",
4
+ "description": "Express adapter for Kizuna.js",
5
+ "keywords": [
6
+ "kizunajs",
7
+ "typescript",
8
+ "zod",
9
+ "openapi",
10
+ "spec-driven",
11
+ "type-safe",
12
+ "rest",
13
+ "api",
14
+ "express",
15
+ "adapter",
16
+ "server"
17
+ ],
18
+ "license": "MIT",
19
+ "homepage": "https://kizunajs.com/docs/adapters/express",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/kizunajs/kizuna.git",
23
+ "directory": "packages/express"
24
+ },
25
+ "bugs": "https://github.com/kizunajs/kizuna/issues",
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "engines": {
29
+ "node": ">=24"
30
+ },
31
+ "main": "./dist/index.cjs",
32
+ "module": "./dist/index.mjs",
33
+ "types": "./dist/index.d.mts",
34
+ "exports": {
35
+ ".": {
36
+ "import": {
37
+ "types": "./dist/index.d.mts",
38
+ "default": "./dist/index.mjs"
39
+ },
40
+ "require": {
41
+ "types": "./dist/index.d.cts",
42
+ "default": "./dist/index.cjs"
43
+ }
44
+ }
45
+ },
46
+ "kizuna": {
47
+ "entries": {
48
+ ".": "server"
49
+ }
50
+ },
51
+ "files": [
52
+ "dist"
53
+ ],
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "peerDependencies": {
58
+ "express": "^5.0.0",
59
+ "zod": "^4.0.0",
60
+ "kizunajs": "2.0.0-beta.0"
61
+ },
62
+ "devDependencies": {
63
+ "@types/express": "^5.0.0",
64
+ "express": "^5.0.0",
65
+ "tsdown": "^0.21.0",
66
+ "typescript": "^6.0.3",
67
+ "zod": "^4.0.0",
68
+ "kizunajs": "2.0.0-beta.0"
69
+ },
70
+ "scripts": {
71
+ "build": "tsdown src/index.ts --format esm,cjs --dts --clean --external kizunajs --external express --external zod",
72
+ "typecheck": "tsc --noEmit"
73
+ }
74
+ }