diff --git a/biome.jsonc b/biome.jsonc
index 25f161f98..425386355 100644
--- a/biome.jsonc
+++ b/biome.jsonc
@@ -20,13 +20,15 @@
"!!**/packages/openapi-code-generator/src/core/schemas/openapi-3.0-specification-validator.ts",
"!!**/packages/openapi-code-generator/src/core/schemas/openapi-3.1-specification-validator.ts",
"!!**/integration-tests/typescript-angular/src/generated/**/*",
+ "!!**/integration-tests/typescript-nextjs/src/app/**/*",
"!!**/integration-tests-definitions/**/*",
"!**/schemas/**/*.json",
"!**/package.json",
"!**/package.json",
"!!**/.next",
"!!**/.angular",
- "!!**/dist"
+ "!!**/dist",
+ "!!**/*.svg"
]
},
"formatter": {
diff --git a/integration-tests/typescript-angular/package.json b/integration-tests/typescript-angular/package.json
index 372011fc8..661e69789 100644
--- a/integration-tests/typescript-angular/package.json
+++ b/integration-tests/typescript-angular/package.json
@@ -16,8 +16,8 @@
"@angular/forms": "catalog:",
"@angular/platform-browser": "catalog:",
"@angular/router": "catalog:",
- "rxjs": "~7.8.0",
- "tslib": "^2.3.0"
+ "rxjs": "~7.8.2",
+ "tslib": "^2.8.1"
},
"devDependencies": {
"@angular/build": "catalog:",
diff --git a/integration-tests/typescript-nextjs/.gitignore b/integration-tests/typescript-nextjs/.gitignore
new file mode 100644
index 000000000..fd3dbb571
--- /dev/null
+++ b/integration-tests/typescript-nextjs/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/integration-tests/typescript-nextjs/README.md b/integration-tests/typescript-nextjs/README.md
new file mode 100644
index 000000000..c4033664f
--- /dev/null
+++ b/integration-tests/typescript-nextjs/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/integration-tests/typescript-nextjs/next.config.mjs b/integration-tests/typescript-nextjs/next.config.mjs
new file mode 100644
index 000000000..1d6147825
--- /dev/null
+++ b/integration-tests/typescript-nextjs/next.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {}
+
+export default nextConfig
diff --git a/integration-tests/typescript-nextjs/package.json b/integration-tests/typescript-nextjs/package.json
new file mode 100644
index 000000000..30ed9a85d
--- /dev/null
+++ b/integration-tests/typescript-nextjs/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@integration/typescript-nextjs",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint",
+ "clean": "rm -rf ./.next && rm -rf ./src/generated && rm -rf -- ./src/app/*/",
+ "validate": "tsc -p ./tsconfig.json"
+ },
+ "dependencies": {
+ "@nahkies/typescript-fetch-runtime": "workspace:*",
+ "@nahkies/typescript-nextjs-runtime": "workspace:*",
+ "joi": "catalog:",
+ "next": "catalog:",
+ "react": "catalog:",
+ "react-dom":"catalog:",
+ "zod": "^4.4.3"
+ },
+ "devDependencies": {
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "^19.2.3",
+ "typescript": "catalog:typescript6"
+ }
+}
diff --git a/integration-tests/typescript-nextjs/public/next.svg b/integration-tests/typescript-nextjs/public/next.svg
new file mode 100644
index 000000000..5174b28c5
--- /dev/null
+++ b/integration-tests/typescript-nextjs/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/integration-tests/typescript-nextjs/public/vercel.svg b/integration-tests/typescript-nextjs/public/vercel.svg
new file mode 100644
index 000000000..d2f842227
--- /dev/null
+++ b/integration-tests/typescript-nextjs/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/integration-tests/typescript-nextjs/src/app/favicon.ico b/integration-tests/typescript-nextjs/src/app/favicon.ico
new file mode 100644
index 000000000..718d6fea4
Binary files /dev/null and b/integration-tests/typescript-nextjs/src/app/favicon.ico differ
diff --git a/integration-tests/typescript-nextjs/src/app/globals.css b/integration-tests/typescript-nextjs/src/app/globals.css
new file mode 100644
index 000000000..acf750815
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/app/globals.css
@@ -0,0 +1,108 @@
+:root {
+ --max-width: 1100px;
+ --border-radius: 12px;
+ --font-mono:
+ ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
+ "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
+ "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
+
+ --foreground-rgb: 0, 0, 0;
+ --background-start-rgb: 214, 219, 220;
+ --background-end-rgb: 255, 255, 255;
+
+ --primary-glow: conic-gradient(
+ from 180deg at 50% 50%,
+ #16abff33 0deg,
+ #0885ff33 55deg,
+ #54d6ff33 120deg,
+ #0071ff33 160deg,
+ transparent 360deg
+ );
+ --secondary-glow: radial-gradient(
+ rgba(255, 255, 255, 1),
+ rgba(255, 255, 255, 0)
+ );
+
+ --tile-start-rgb: 239, 245, 249;
+ --tile-end-rgb: 228, 232, 233;
+ --tile-border: conic-gradient(
+ #00000080,
+ #00000040,
+ #00000030,
+ #00000020,
+ #00000010,
+ #00000010,
+ #00000080
+ );
+
+ --callout-rgb: 238, 240, 241;
+ --callout-border-rgb: 172, 175, 176;
+ --card-rgb: 180, 185, 188;
+ --card-border-rgb: 131, 134, 135;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --foreground-rgb: 255, 255, 255;
+ --background-start-rgb: 0, 0, 0;
+ --background-end-rgb: 0, 0, 0;
+
+ --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
+ --secondary-glow: linear-gradient(
+ to bottom right,
+ rgba(1, 65, 255, 0),
+ rgba(1, 65, 255, 0),
+ rgba(1, 65, 255, 0.3)
+ );
+
+ --tile-start-rgb: 2, 13, 46;
+ --tile-end-rgb: 2, 5, 19;
+ --tile-border: conic-gradient(
+ #ffffff80,
+ #ffffff40,
+ #ffffff30,
+ #ffffff20,
+ #ffffff10,
+ #ffffff10,
+ #ffffff80
+ );
+
+ --callout-rgb: 20, 20, 20;
+ --callout-border-rgb: 108, 108, 108;
+ --card-rgb: 100, 100, 100;
+ --card-border-rgb: 200, 200, 200;
+ }
+}
+
+* {
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+}
+
+html,
+body {
+ max-width: 100vw;
+ overflow-x: hidden;
+}
+
+body {
+ color: rgb(var(--foreground-rgb));
+ background: linear-gradient(
+ to bottom,
+ transparent,
+ rgb(var(--background-end-rgb))
+ )
+ rgb(var(--background-start-rgb));
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ color-scheme: dark;
+ }
+}
diff --git a/integration-tests/typescript-nextjs/src/app/layout.tsx b/integration-tests/typescript-nextjs/src/app/layout.tsx
new file mode 100644
index 000000000..24de49cbb
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/app/layout.tsx
@@ -0,0 +1,22 @@
+import type {Metadata} from "next"
+import {Inter} from "next/font/google"
+import "./globals.css"
+
+const inter = Inter({subsets: ["latin"]})
+
+export const metadata: Metadata = {
+ title: "Create Next App",
+ description: "Generated by create next app",
+}
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode
+}>) {
+ return (
+
+
{children}
+
+ )
+}
diff --git a/integration-tests/typescript-nextjs/src/app/page.module.css b/integration-tests/typescript-nextjs/src/app/page.module.css
new file mode 100644
index 000000000..d979f776c
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/app/page.module.css
@@ -0,0 +1,232 @@
+.main {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ padding: 6rem;
+ min-height: 100vh;
+}
+
+.description {
+ display: inherit;
+ justify-content: inherit;
+ align-items: inherit;
+ font-size: 0.85rem;
+ max-width: var(--max-width);
+ width: 100%;
+ z-index: 2;
+ font-family: var(--font-mono);
+}
+
+.description a {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.description p {
+ position: relative;
+ margin: 0;
+ padding: 1rem;
+ background-color: rgba(var(--callout-rgb), 0.5);
+ border: 1px solid rgba(var(--callout-border-rgb), 0.3);
+ border-radius: var(--border-radius);
+}
+
+.code {
+ font-weight: 700;
+ font-family: var(--font-mono);
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(25%, auto));
+ max-width: 100%;
+ width: var(--max-width);
+}
+
+.card {
+ padding: 1rem 1.2rem;
+ border-radius: var(--border-radius);
+ background: rgba(var(--card-rgb), 0);
+ border: 1px solid rgba(var(--card-border-rgb), 0);
+ transition:
+ background 200ms,
+ border 200ms;
+}
+
+.card span {
+ display: inline-block;
+ transition: transform 200ms;
+}
+
+.card h2 {
+ font-weight: 600;
+ margin-bottom: 0.7rem;
+}
+
+.card p {
+ margin: 0;
+ opacity: 0.6;
+ font-size: 0.9rem;
+ line-height: 1.5;
+ max-width: 30ch;
+ text-wrap: balance;
+}
+
+.center {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ padding: 4rem 0;
+}
+
+.center::before {
+ background: var(--secondary-glow);
+ border-radius: 50%;
+ width: 480px;
+ height: 360px;
+ margin-left: -400px;
+}
+
+.center::after {
+ background: var(--primary-glow);
+ width: 240px;
+ height: 180px;
+ z-index: -1;
+}
+
+.center::before,
+.center::after {
+ content: "";
+ left: 50%;
+ position: absolute;
+ filter: blur(45px);
+ transform: translateZ(0);
+}
+
+.logo {
+ position: relative;
+}
+/* Enable hover only on non-touch devices */
+@media (hover: hover) and (pointer: fine) {
+ .card:hover {
+ background: rgba(var(--card-rgb), 0.1);
+ border: 1px solid rgba(var(--card-border-rgb), 0.15);
+ }
+
+ .card:hover span {
+ transform: translateX(4px);
+ }
+}
+
+@media (prefers-reduced-motion) {
+ .card:hover span {
+ transform: none;
+ }
+}
+
+/* Mobile */
+@media (max-width: 700px) {
+ .content {
+ padding: 4rem;
+ }
+
+ .grid {
+ grid-template-columns: 1fr;
+ margin-bottom: 120px;
+ max-width: 320px;
+ text-align: center;
+ }
+
+ .card {
+ padding: 1rem 2.5rem;
+ }
+
+ .card h2 {
+ margin-bottom: 0.5rem;
+ }
+
+ .center {
+ padding: 8rem 0 6rem;
+ }
+
+ .center::before {
+ transform: none;
+ height: 300px;
+ }
+
+ .description {
+ font-size: 0.8rem;
+ }
+
+ .description a {
+ padding: 1rem;
+ }
+
+ .description p,
+ .description div {
+ display: flex;
+ justify-content: center;
+ position: fixed;
+ width: 100%;
+ }
+
+ .description p {
+ align-items: center;
+ inset: 0 0 auto;
+ padding: 2rem 1rem 1.4rem;
+ border-radius: 0;
+ border: none;
+ border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
+ background: linear-gradient(
+ to bottom,
+ rgba(var(--background-start-rgb), 1),
+ rgba(var(--callout-rgb), 0.5)
+ );
+ background-clip: padding-box;
+ backdrop-filter: blur(24px);
+ }
+
+ .description div {
+ align-items: flex-end;
+ pointer-events: none;
+ inset: auto 0 0;
+ padding: 2rem;
+ height: 200px;
+ background: linear-gradient(
+ to bottom,
+ transparent 0%,
+ rgb(var(--background-end-rgb)) 40%
+ );
+ z-index: 1;
+ }
+}
+
+/* Tablet and Smaller Desktop */
+@media (min-width: 701px) and (max-width: 1120px) {
+ .grid {
+ grid-template-columns: repeat(2, 50%);
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ .vercelLogo {
+ filter: invert(1);
+ }
+
+ .logo {
+ filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
+ }
+}
+
+@keyframes rotate {
+ from {
+ transform: rotate(360deg);
+ }
+ to {
+ transform: rotate(0deg);
+ }
+}
diff --git a/integration-tests/typescript-nextjs/src/app/page.tsx b/integration-tests/typescript-nextjs/src/app/page.tsx
new file mode 100644
index 000000000..c6b15f1be
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/app/page.tsx
@@ -0,0 +1,95 @@
+import Image from "next/image"
+import styles from "./page.module.css"
+
+export default function Home() {
+ return (
+
+
+
+ Get started by editing
+ src/app/page.tsx
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/[id]/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/[id]/route.ts
new file mode 100644
index 000000000..aae0592e4
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/[id]/route.ts
@@ -0,0 +1,14 @@
+import {_PUT} from "@/generated/todo-lists.yaml/attachments/[id]/route"
+
+export const PUT = _PUT(
+ async ({params, body}, respond, request) => {
+ // TODO: implementation
+ return respond.withStatus(501).body({message: "not implemented"} as any)
+ },
+ async (err) => {
+ // TODO: implementation
+ return new Response(JSON.stringify({message: "not implemented"}), {
+ status: 501,
+ })
+ },
+)
diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts
new file mode 100644
index 000000000..aa413967d
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts
@@ -0,0 +1,26 @@
+import {_GET, _POST} from "@/generated/todo-lists.yaml/attachments/route"
+
+export const GET = _GET(
+ async (respond, request) => {
+ // TODO: implementation
+ return respond.withStatus(501).body({message: "not implemented"} as any)
+ },
+ async (err) => {
+ // TODO: implementation
+ return new Response(JSON.stringify({message: "not implemented"}), {
+ status: 501,
+ })
+ },
+)
+export const POST = _POST(
+ async ({body}, respond, request) => {
+ // TODO: implementation
+ return respond.withStatus(501).body({message: "not implemented"} as any)
+ },
+ async (err) => {
+ // TODO: implementation
+ return new Response(JSON.stringify({message: "not implemented"}), {
+ status: 501,
+ })
+ },
+)
diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts
new file mode 100644
index 000000000..15aec8628
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts
@@ -0,0 +1,29 @@
+import {
+ _GET,
+ _POST,
+} from "@/generated/todo-lists.yaml/list/[listId]/items/route"
+
+export const GET = _GET(
+ async ({params}, respond, request) => {
+ // TODO: implementation
+ return respond.withStatus(501).body({message: "not implemented"} as any)
+ },
+ async (err) => {
+ // TODO: implementation
+ return new Response(JSON.stringify({message: "not implemented"}), {
+ status: 501,
+ })
+ },
+)
+export const POST = _POST(
+ async ({params, body}, respond, request) => {
+ // TODO: implementation
+ return respond.withStatus(501).body({message: "not implemented"} as any)
+ },
+ async (err) => {
+ // TODO: implementation
+ return new Response(JSON.stringify({message: "not implemented"}), {
+ status: 501,
+ })
+ },
+)
diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts
new file mode 100644
index 000000000..c7d404f79
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts
@@ -0,0 +1,42 @@
+import {
+ _DELETE,
+ _GET,
+ _PUT,
+} from "@/generated/todo-lists.yaml/list/[listId]/route"
+
+export const GET = _GET(
+ async ({params}, respond, request) => {
+ // TODO: implementation
+ return respond.withStatus(501).body({message: "not implemented"} as any)
+ },
+ async (err) => {
+ // TODO: implementation
+ return new Response(JSON.stringify({message: "not implemented"}), {
+ status: 501,
+ })
+ },
+)
+export const PUT = _PUT(
+ async ({params, body}, respond, request) => {
+ // TODO: implementation
+ return respond.withStatus(501).body({message: "not implemented"} as any)
+ },
+ async (err) => {
+ // TODO: implementation
+ return new Response(JSON.stringify({message: "not implemented"}), {
+ status: 501,
+ })
+ },
+)
+export const DELETE = _DELETE(
+ async ({params}, respond, request) => {
+ // TODO: implementation
+ return respond.withStatus(501).body({message: "not implemented"} as any)
+ },
+ async (err) => {
+ // TODO: implementation
+ return new Response(JSON.stringify({message: "not implemented"}), {
+ status: 501,
+ })
+ },
+)
diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts
new file mode 100644
index 000000000..be9556729
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts
@@ -0,0 +1,14 @@
+import {_GET} from "@/generated/todo-lists.yaml/list/route"
+
+export const GET = _GET(
+ async ({query}, respond, request) => {
+ // TODO: implementation
+ return respond.withStatus(501).body({message: "not implemented"} as any)
+ },
+ async (err) => {
+ // TODO: implementation
+ return new Response(JSON.stringify({message: "not implemented"}), {
+ status: 501,
+ })
+ },
+)
diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/[id]/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/[id]/route.ts
new file mode 100644
index 000000000..b23b0e1e4
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/[id]/route.ts
@@ -0,0 +1,93 @@
+/** AUTOGENERATED - DO NOT EDIT **/
+/* tslint:disable */
+/* eslint-disable */
+
+import {
+ OpenAPIRuntimeError,
+ RequestInputType,
+} from "@nahkies/typescript-nextjs-runtime/errors"
+import {
+ type OpenAPIRuntimeResponder,
+ OpenAPIRuntimeResponse,
+ type Params,
+ type StatusCode,
+} from "@nahkies/typescript-nextjs-runtime/server"
+import {
+ parseRequestInput,
+ responseValidationFactory,
+} from "@nahkies/typescript-nextjs-runtime/zod-v4"
+import type {NextRequest} from "next/server"
+import {z} from "zod/v4"
+import type {t_ReplaceAttachmentParamSchema} from "@/generated/todo-lists.yaml/models"
+
+// /attachments/{id}
+export type ReplaceAttachmentResponder = {
+ with202(): OpenAPIRuntimeResponse
+} & OpenAPIRuntimeResponder
+
+export type ReplaceAttachment = (
+ params: Params,
+ respond: ReplaceAttachmentResponder,
+ request: NextRequest,
+) => Promise>
+
+const replaceAttachmentParamSchema = z.object({id: z.string()})
+
+export const _PUT =
+ (
+ implementation: ReplaceAttachment,
+ onError: (err: unknown) => Promise,
+ ) =>
+ async (
+ request: NextRequest,
+ {params}: {params: Promise},
+ ): Promise => {
+ try {
+ const input = {
+ params: parseRequestInput(
+ replaceAttachmentParamSchema,
+ await params,
+ RequestInputType.RouteParam,
+ ),
+ query: undefined,
+ body: parseRequestInput(
+ z.any(),
+ await request.blob(),
+ RequestInputType.RequestBody,
+ ),
+ headers: undefined,
+ }
+ const responder = {
+ with202() {
+ return new OpenAPIRuntimeResponse(202)
+ },
+ withStatus(status: StatusCode) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ }
+ const responseValidator = responseValidationFactory(
+ [["202", z.undefined()]],
+ undefined,
+ )
+
+ const res = await implementation(input, responder, request)
+ .then((it) => {
+ if (it instanceof Response) {
+ return it
+ }
+ const {status, body} = it.unpack()
+ const validatedBody = responseValidator(status, body)
+
+ return validatedBody !== undefined
+ ? Response.json(validatedBody, {status})
+ : new Response(undefined, {status})
+ })
+ .catch((err) => {
+ throw OpenAPIRuntimeError.HandlerError(err)
+ })
+
+ return res
+ } catch (err) {
+ return await onError(err)
+ }
+ }
diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts
new file mode 100644
index 000000000..5609c9a6a
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts
@@ -0,0 +1,137 @@
+/** AUTOGENERATED - DO NOT EDIT **/
+/* tslint:disable */
+/* eslint-disable */
+
+import {
+ OpenAPIRuntimeError,
+ RequestInputType,
+} from "@nahkies/typescript-nextjs-runtime/errors"
+import {
+ type OpenAPIRuntimeResponder,
+ OpenAPIRuntimeResponse,
+ type Params,
+ type StatusCode,
+} from "@nahkies/typescript-nextjs-runtime/server"
+import {
+ parseRequestInput,
+ responseValidationFactory,
+} from "@nahkies/typescript-nextjs-runtime/zod-v4"
+import type {NextRequest} from "next/server"
+import {z} from "zod/v4"
+import type {t_UnknownObject} from "@/generated/todo-lists.yaml/models"
+import {s_UnknownObject} from "@/generated/todo-lists.yaml/schemas"
+
+// /attachments
+export type ListAttachmentsResponder = {
+ with200(): OpenAPIRuntimeResponse
+} & OpenAPIRuntimeResponder
+
+export type ListAttachments = (
+ respond: ListAttachmentsResponder,
+ request: NextRequest,
+) => Promise>
+
+export type UploadAttachmentResponder = {
+ with202(): OpenAPIRuntimeResponse
+} & OpenAPIRuntimeResponder
+
+export type UploadAttachment = (
+ params: Params,
+ respond: UploadAttachmentResponder,
+ request: NextRequest,
+) => Promise>
+
+export const _GET =
+ (
+ implementation: ListAttachments,
+ onError: (err: unknown) => Promise,
+ ) =>
+ async (request: NextRequest): Promise => {
+ try {
+ const responder = {
+ with200() {
+ return new OpenAPIRuntimeResponse(200)
+ },
+ withStatus(status: StatusCode) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ }
+ const responseValidator = responseValidationFactory(
+ [["200", z.array(s_UnknownObject)]],
+ undefined,
+ )
+
+ const res = await implementation(responder, request)
+ .then((it) => {
+ if (it instanceof Response) {
+ return it
+ }
+ const {status, body} = it.unpack()
+ const validatedBody = responseValidator(status, body)
+
+ return validatedBody !== undefined
+ ? Response.json(validatedBody, {status})
+ : new Response(undefined, {status})
+ })
+ .catch((err) => {
+ throw OpenAPIRuntimeError.HandlerError(err)
+ })
+
+ return res
+ } catch (err) {
+ return await onError(err)
+ }
+ }
+
+export const _POST =
+ (
+ implementation: UploadAttachment,
+ onError: (err: unknown) => Promise,
+ ) =>
+ async (request: NextRequest): Promise => {
+ try {
+ const input = {
+ params: undefined,
+ query: undefined,
+ // todo: request bodies with content-type 'multipart/form-data' not yet supported
+ body: parseRequestInput(
+ z.never(),
+ await request.formData(),
+ RequestInputType.RequestBody,
+ ) as never,
+ headers: undefined,
+ }
+ const responder = {
+ with202() {
+ return new OpenAPIRuntimeResponse(202)
+ },
+ withStatus(status: StatusCode) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ }
+ const responseValidator = responseValidationFactory(
+ [["202", z.undefined()]],
+ undefined,
+ )
+
+ const res = await implementation(input, responder, request)
+ .then((it) => {
+ if (it instanceof Response) {
+ return it
+ }
+ const {status, body} = it.unpack()
+ const validatedBody = responseValidator(status, body)
+
+ return validatedBody !== undefined
+ ? Response.json(validatedBody, {status})
+ : new Response(undefined, {status})
+ })
+ .catch((err) => {
+ throw OpenAPIRuntimeError.HandlerError(err)
+ })
+
+ return res
+ } catch (err) {
+ return await onError(err)
+ }
+ }
diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/client.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/client.ts
new file mode 100644
index 000000000..4d7e747bd
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/client.ts
@@ -0,0 +1,345 @@
+/** AUTOGENERATED - DO NOT EDIT **/
+/* tslint:disable */
+/* eslint-disable */
+
+import {
+ AbstractFetchClient,
+ type AbstractFetchClientConfig,
+ type Res,
+ type Server,
+ type StatusCode,
+ type StatusCode4xx,
+ type StatusCode5xx,
+} from "@nahkies/typescript-fetch-runtime/main"
+import type {
+ t_CreateTodoListItemRequestBody,
+ t_CreateUpdateTodoList,
+ t_Error,
+ t_Statuses,
+ t_TodoList,
+ t_UnknownObject,
+} from "@/generated/todo-lists.yaml/models"
+
+export class ApiClientServersOperations {
+ static listAttachments(url?: "{schema}://{tenant}.attachments.example.com"): {
+ build: (
+ schema?: "http" | "https",
+ tenant?: string,
+ ) => Server<"listAttachments_ApiClient">
+ }
+ static listAttachments(url?: "https://attachments.example.com"): {
+ build: () => Server<"listAttachments_ApiClient">
+ }
+ static listAttachments(
+ url: string = "{schema}://{tenant}.attachments.example.com",
+ ): unknown {
+ switch (url) {
+ case "{schema}://{tenant}.attachments.example.com":
+ return {
+ build(
+ schema: "http" | "https" = "https",
+ tenant = "your-slug",
+ ): Server<"listAttachments_ApiClient"> {
+ return "{schema}://{tenant}.attachments.example.com"
+ .replace("{schema}", schema)
+ .replace(
+ "{tenant}",
+ tenant,
+ ) as Server<"listAttachments_ApiClient">
+ },
+ }
+
+ case "https://attachments.example.com":
+ return {
+ build(): Server<"listAttachments_ApiClient"> {
+ return "https://attachments.example.com" as Server<"listAttachments_ApiClient">
+ },
+ }
+
+ default:
+ throw new Error(`no matching server for url '${url}'`)
+ }
+ }
+
+ static uploadAttachment(
+ url?: "{schema}://{tenant}.attachments.example.com",
+ ): {
+ build: (
+ schema?: "http" | "https",
+ tenant?: string,
+ ) => Server<"uploadAttachment_ApiClient">
+ }
+ static uploadAttachment(url?: "https://attachments.example.com"): {
+ build: () => Server<"uploadAttachment_ApiClient">
+ }
+ static uploadAttachment(
+ url: string = "{schema}://{tenant}.attachments.example.com",
+ ): unknown {
+ switch (url) {
+ case "{schema}://{tenant}.attachments.example.com":
+ return {
+ build(
+ schema: "http" | "https" = "https",
+ tenant = "your-slug",
+ ): Server<"uploadAttachment_ApiClient"> {
+ return "{schema}://{tenant}.attachments.example.com"
+ .replace("{schema}", schema)
+ .replace(
+ "{tenant}",
+ tenant,
+ ) as Server<"uploadAttachment_ApiClient">
+ },
+ }
+
+ case "https://attachments.example.com":
+ return {
+ build(): Server<"uploadAttachment_ApiClient"> {
+ return "https://attachments.example.com" as Server<"uploadAttachment_ApiClient">
+ },
+ }
+
+ default:
+ throw new Error(`no matching server for url '${url}'`)
+ }
+ }
+}
+
+export class ApiClientServers {
+ static default(): Server<"ApiClient"> {
+ return ApiClientServers.server().build()
+ }
+
+ static server(url?: "{schema}://{tenant}.todo-lists.example.com"): {
+ build: (schema?: "http" | "https", tenant?: string) => Server<"ApiClient">
+ }
+ static server(url?: "https://todo-lists.example.com"): {
+ build: () => Server<"ApiClient">
+ }
+ static server(
+ url: string = "{schema}://{tenant}.todo-lists.example.com",
+ ): unknown {
+ switch (url) {
+ case "{schema}://{tenant}.todo-lists.example.com":
+ return {
+ build(
+ schema: "http" | "https" = "https",
+ tenant = "your-slug",
+ ): Server<"ApiClient"> {
+ return "{schema}://{tenant}.todo-lists.example.com"
+ .replace("{schema}", schema)
+ .replace("{tenant}", tenant) as Server<"ApiClient">
+ },
+ }
+
+ case "https://todo-lists.example.com":
+ return {
+ build(): Server<"ApiClient"> {
+ return "https://todo-lists.example.com" as Server<"ApiClient">
+ },
+ }
+
+ default:
+ throw new Error(`no matching server for url '${url}'`)
+ }
+ }
+
+ static readonly operations = ApiClientServersOperations
+}
+
+export interface ApiClientConfig extends AbstractFetchClientConfig {
+ basePath: Server<"ApiClient"> | string
+}
+
+export class ApiClient extends AbstractFetchClient {
+ constructor(config: ApiClientConfig) {
+ super(config)
+ }
+
+ async getTodoLists(
+ p: {
+ created?: string
+ statuses?: t_Statuses
+ tags?: string[]
+ } = {},
+ timeout?: number,
+ opts: RequestInit = {},
+ ): Promise> {
+ const url = this.basePath + `/list`
+ const headers = this._headers({Accept: "application/json"}, opts.headers)
+ const query = this._query(
+ {created: p["created"], statuses: p["statuses"], tags: p["tags"]},
+ {
+ statuses: {
+ style: "form",
+ explode: true,
+ },
+ tags: {
+ style: "form",
+ explode: true,
+ },
+ },
+ )
+
+ return this._fetch(url + query, {method: "GET", ...opts, headers}, timeout)
+ }
+
+ async getTodoListById(
+ p: {
+ listId: string
+ },
+ timeout?: number,
+ opts: RequestInit = {},
+ ): Promise<
+ | Res<200, t_TodoList>
+ | Res
+ | Res, void>
+ > {
+ const url = this.basePath + `/list/${p["listId"]}`
+ const headers = this._headers({Accept: "application/json"}, opts.headers)
+
+ return this._fetch(url, {method: "GET", ...opts, headers}, timeout)
+ }
+
+ async updateTodoListById(
+ p: {
+ listId: string
+ requestBody: t_CreateUpdateTodoList
+ },
+ timeout?: number,
+ opts: RequestInit = {},
+ ): Promise<
+ | Res<200, t_TodoList>
+ | Res
+ | Res, void>
+ > {
+ const url = this.basePath + `/list/${p["listId"]}`
+ const headers = this._headers(
+ {Accept: "application/json", "Content-Type": "application/json"},
+ opts.headers,
+ )
+ const body = JSON.stringify(p.requestBody)
+
+ return this._fetch(url, {method: "PUT", body, ...opts, headers}, timeout)
+ }
+
+ async deleteTodoListById(
+ p: {
+ listId: string
+ },
+ timeout?: number,
+ opts: RequestInit = {},
+ ): Promise<
+ | Res<204, void>
+ | Res
+ | Res, void>
+ > {
+ const url = this.basePath + `/list/${p["listId"]}`
+ const headers = this._headers({Accept: "application/json"}, opts.headers)
+
+ return this._fetch(url, {method: "DELETE", ...opts, headers}, timeout)
+ }
+
+ async getTodoListItems(
+ p: {
+ listId: string
+ },
+ timeout?: number,
+ opts: RequestInit = {},
+ ): Promise<
+ | Res<
+ 200,
+ {
+ completedAt?: string
+ content: string
+ createdAt: string
+ id: string
+ }
+ >
+ | Res<
+ StatusCode5xx,
+ {
+ code: string
+ message: string
+ }
+ >
+ > {
+ const url = this.basePath + `/list/${p["listId"]}/items`
+ const headers = this._headers({Accept: "application/json"}, opts.headers)
+
+ return this._fetch(url, {method: "GET", ...opts, headers}, timeout)
+ }
+
+ async createTodoListItem(
+ p: {
+ listId: string
+ requestBody: t_CreateTodoListItemRequestBody
+ },
+ timeout?: number,
+ opts: RequestInit = {},
+ ): Promise> {
+ const url = this.basePath + `/list/${p["listId"]}/items`
+ const headers = this._headers(
+ {Accept: "application/json", "Content-Type": "application/json"},
+ opts.headers,
+ )
+ const body = JSON.stringify(p.requestBody)
+
+ return this._fetch(url, {method: "POST", body, ...opts, headers}, timeout)
+ }
+
+ async listAttachments(
+ basePath:
+ | Server<"listAttachments_ApiClient">
+ | string = ApiClientServers.operations.listAttachments().build(),
+ timeout?: number,
+ opts: RequestInit = {},
+ ): Promise> {
+ const url = basePath + `/attachments`
+ const headers = this._headers({Accept: "application/json"}, opts.headers)
+
+ return this._fetch(url, {method: "GET", ...opts, headers}, timeout)
+ }
+
+ async uploadAttachment(
+ p: {
+ requestBody: never
+ },
+ basePath:
+ | Server<"uploadAttachment_ApiClient">
+ | string = ApiClientServers.operations.uploadAttachment().build(),
+ timeout?: number,
+ opts: RequestInit = {},
+ ): Promise> {
+ const url = basePath + `/attachments`
+ const headers = this._headers({Accept: "application/json"}, opts.headers)
+
+ return this._fetch(
+ url,
+ {
+ method: "POST",
+ // todo: request bodies with content-type 'multipart/form-data' not yet supported,
+ ...opts,
+ headers,
+ },
+ timeout,
+ )
+ }
+
+ async replaceAttachment(
+ p: {
+ id: string
+ requestBody: Blob
+ },
+ timeout?: number,
+ opts: RequestInit = {},
+ ): Promise> {
+ const url = this.basePath + `/attachments/${p["id"]}`
+ const headers = this._headers(
+ {Accept: "application/json", "Content-Type": "application/octet-stream"},
+ opts.headers,
+ )
+ const body = p.requestBody
+
+ return this._fetch(url, {method: "PUT", body, ...opts, headers}, timeout)
+ }
+}
diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts
new file mode 100644
index 000000000..72aa20b70
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts
@@ -0,0 +1,202 @@
+/** AUTOGENERATED - DO NOT EDIT **/
+/* tslint:disable */
+/* eslint-disable */
+
+import {
+ OpenAPIRuntimeError,
+ RequestInputType,
+} from "@nahkies/typescript-nextjs-runtime/errors"
+import {
+ type OpenAPIRuntimeResponder,
+ OpenAPIRuntimeResponse,
+ type Params,
+ type StatusCode,
+ type StatusCode5xx,
+} from "@nahkies/typescript-nextjs-runtime/server"
+import {
+ parseRequestInput,
+ responseValidationFactory,
+} from "@nahkies/typescript-nextjs-runtime/zod-v4"
+import type {NextRequest} from "next/server"
+import {z} from "zod/v4"
+import type {
+ t_CreateTodoListItemParamSchema,
+ t_CreateTodoListItemRequestBody,
+ t_GetTodoListItemsParamSchema,
+} from "@/generated/todo-lists.yaml/models"
+import {s_CreateTodoListItemRequestBody} from "@/generated/todo-lists.yaml/schemas"
+
+// /list/{listId}/items
+export type GetTodoListItemsResponder = {
+ with200(): OpenAPIRuntimeResponse<{
+ completedAt?: string
+ content: string
+ createdAt: string
+ id: string
+ }>
+ withStatusCode5xx(status: StatusCode5xx): OpenAPIRuntimeResponse<{
+ code: string
+ message: string
+ }>
+} & OpenAPIRuntimeResponder
+
+export type GetTodoListItems = (
+ params: Params,
+ respond: GetTodoListItemsResponder,
+ request: NextRequest,
+) => Promise>
+
+export type CreateTodoListItemResponder = {
+ with204(): OpenAPIRuntimeResponse
+} & OpenAPIRuntimeResponder
+
+export type CreateTodoListItem = (
+ params: Params<
+ t_CreateTodoListItemParamSchema,
+ void,
+ t_CreateTodoListItemRequestBody,
+ void
+ >,
+ respond: CreateTodoListItemResponder,
+ request: NextRequest,
+) => Promise>
+
+const getTodoListItemsParamSchema = z.object({listId: z.string()})
+
+export const _GET =
+ (
+ implementation: GetTodoListItems,
+ onError: (err: unknown) => Promise,
+ ) =>
+ async (
+ request: NextRequest,
+ {params}: {params: Promise},
+ ): Promise => {
+ try {
+ const input = {
+ params: parseRequestInput(
+ getTodoListItemsParamSchema,
+ await params,
+ RequestInputType.RouteParam,
+ ),
+ query: undefined,
+ body: undefined,
+ headers: undefined,
+ }
+ const responder = {
+ with200() {
+ return new OpenAPIRuntimeResponse<{
+ completedAt?: string
+ content: string
+ createdAt: string
+ id: string
+ }>(200)
+ },
+ withStatusCode5xx(status: StatusCode5xx) {
+ return new OpenAPIRuntimeResponse<{
+ code: string
+ message: string
+ }>(status)
+ },
+ withStatus(status: StatusCode) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ }
+ const responseValidator = responseValidationFactory(
+ [
+ [
+ "200",
+ z.object({
+ id: z.string(),
+ content: z.string(),
+ createdAt: z.iso.datetime({offset: true}),
+ completedAt: z.iso.datetime({offset: true}).optional(),
+ }),
+ ],
+ ["5XX", z.object({message: z.string(), code: z.string()})],
+ ],
+ undefined,
+ )
+
+ const res = await implementation(input, responder, request)
+ .then((it) => {
+ if (it instanceof Response) {
+ return it
+ }
+ const {status, body} = it.unpack()
+ const validatedBody = responseValidator(status, body)
+
+ return validatedBody !== undefined
+ ? Response.json(validatedBody, {status})
+ : new Response(undefined, {status})
+ })
+ .catch((err) => {
+ throw OpenAPIRuntimeError.HandlerError(err)
+ })
+
+ return res
+ } catch (err) {
+ return await onError(err)
+ }
+ }
+
+const createTodoListItemParamSchema = z.object({listId: z.string()})
+
+export const _POST =
+ (
+ implementation: CreateTodoListItem,
+ onError: (err: unknown) => Promise,
+ ) =>
+ async (
+ request: NextRequest,
+ {params}: {params: Promise},
+ ): Promise => {
+ try {
+ const input = {
+ params: parseRequestInput(
+ createTodoListItemParamSchema,
+ await params,
+ RequestInputType.RouteParam,
+ ),
+ query: undefined,
+ body: parseRequestInput(
+ s_CreateTodoListItemRequestBody,
+ await request.json(),
+ RequestInputType.RequestBody,
+ ),
+ headers: undefined,
+ }
+ const responder = {
+ with204() {
+ return new OpenAPIRuntimeResponse(204)
+ },
+ withStatus(status: StatusCode) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ }
+ const responseValidator = responseValidationFactory(
+ [["204", z.undefined()]],
+ undefined,
+ )
+
+ const res = await implementation(input, responder, request)
+ .then((it) => {
+ if (it instanceof Response) {
+ return it
+ }
+ const {status, body} = it.unpack()
+ const validatedBody = responseValidator(status, body)
+
+ return validatedBody !== undefined
+ ? Response.json(validatedBody, {status})
+ : new Response(undefined, {status})
+ })
+ .catch((err) => {
+ throw OpenAPIRuntimeError.HandlerError(err)
+ })
+
+ return res
+ } catch (err) {
+ return await onError(err)
+ }
+ }
diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts
new file mode 100644
index 000000000..a86b3aa36
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts
@@ -0,0 +1,278 @@
+/** AUTOGENERATED - DO NOT EDIT **/
+/* tslint:disable */
+/* eslint-disable */
+
+import {
+ OpenAPIRuntimeError,
+ RequestInputType,
+} from "@nahkies/typescript-nextjs-runtime/errors"
+import {
+ type OpenAPIRuntimeResponder,
+ OpenAPIRuntimeResponse,
+ type Params,
+ type StatusCode,
+ type StatusCode4xx,
+} from "@nahkies/typescript-nextjs-runtime/server"
+import {
+ parseRequestInput,
+ responseValidationFactory,
+} from "@nahkies/typescript-nextjs-runtime/zod-v4"
+import type {NextRequest} from "next/server"
+import {z} from "zod/v4"
+import type {
+ t_CreateUpdateTodoList,
+ t_DeleteTodoListByIdParamSchema,
+ t_Error,
+ t_GetTodoListByIdParamSchema,
+ t_TodoList,
+ t_UpdateTodoListByIdParamSchema,
+} from "@/generated/todo-lists.yaml/models"
+import {
+ s_CreateUpdateTodoList,
+ s_Error,
+ s_TodoList,
+} from "@/generated/todo-lists.yaml/schemas"
+
+// /list/{listId}
+export type GetTodoListByIdResponder = {
+ with200(): OpenAPIRuntimeResponse
+ withStatusCode4xx(status: StatusCode4xx): OpenAPIRuntimeResponse
+ withDefault(status: StatusCode): OpenAPIRuntimeResponse
+} & OpenAPIRuntimeResponder
+
+export type GetTodoListById = (
+ params: Params,
+ respond: GetTodoListByIdResponder,
+ request: NextRequest,
+) => Promise>
+
+export type UpdateTodoListByIdResponder = {
+ with200(): OpenAPIRuntimeResponse
+ withStatusCode4xx(status: StatusCode4xx): OpenAPIRuntimeResponse
+ withDefault(status: StatusCode): OpenAPIRuntimeResponse
+} & OpenAPIRuntimeResponder
+
+export type UpdateTodoListById = (
+ params: Params<
+ t_UpdateTodoListByIdParamSchema,
+ void,
+ t_CreateUpdateTodoList,
+ void
+ >,
+ respond: UpdateTodoListByIdResponder,
+ request: NextRequest,
+) => Promise>
+
+export type DeleteTodoListByIdResponder = {
+ with204(): OpenAPIRuntimeResponse
+ withStatusCode4xx(status: StatusCode4xx): OpenAPIRuntimeResponse
+ withDefault(status: StatusCode): OpenAPIRuntimeResponse
+} & OpenAPIRuntimeResponder
+
+export type DeleteTodoListById = (
+ params: Params,
+ respond: DeleteTodoListByIdResponder,
+ request: NextRequest,
+) => Promise>
+
+const getTodoListByIdParamSchema = z.object({listId: z.string()})
+
+export const _GET =
+ (
+ implementation: GetTodoListById,
+ onError: (err: unknown) => Promise,
+ ) =>
+ async (
+ request: NextRequest,
+ {params}: {params: Promise},
+ ): Promise => {
+ try {
+ const input = {
+ params: parseRequestInput(
+ getTodoListByIdParamSchema,
+ await params,
+ RequestInputType.RouteParam,
+ ),
+ query: undefined,
+ body: undefined,
+ headers: undefined,
+ }
+ const responder = {
+ with200() {
+ return new OpenAPIRuntimeResponse(200)
+ },
+ withStatusCode4xx(status: StatusCode4xx) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ withDefault(status: StatusCode) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ withStatus(status: StatusCode) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ }
+ const responseValidator = responseValidationFactory(
+ [
+ ["200", s_TodoList],
+ ["4XX", s_Error],
+ ],
+ z.undefined(),
+ )
+
+ const res = await implementation(input, responder, request)
+ .then((it) => {
+ if (it instanceof Response) {
+ return it
+ }
+ const {status, body} = it.unpack()
+ const validatedBody = responseValidator(status, body)
+
+ return validatedBody !== undefined
+ ? Response.json(validatedBody, {status})
+ : new Response(undefined, {status})
+ })
+ .catch((err) => {
+ throw OpenAPIRuntimeError.HandlerError(err)
+ })
+
+ return res
+ } catch (err) {
+ return await onError(err)
+ }
+ }
+
+const updateTodoListByIdParamSchema = z.object({listId: z.string()})
+
+export const _PUT =
+ (
+ implementation: UpdateTodoListById,
+ onError: (err: unknown) => Promise,
+ ) =>
+ async (
+ request: NextRequest,
+ {params}: {params: Promise},
+ ): Promise => {
+ try {
+ const input = {
+ params: parseRequestInput(
+ updateTodoListByIdParamSchema,
+ await params,
+ RequestInputType.RouteParam,
+ ),
+ query: undefined,
+ body: parseRequestInput(
+ s_CreateUpdateTodoList,
+ await request.json(),
+ RequestInputType.RequestBody,
+ ),
+ headers: undefined,
+ }
+ const responder = {
+ with200() {
+ return new OpenAPIRuntimeResponse(200)
+ },
+ withStatusCode4xx(status: StatusCode4xx) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ withDefault(status: StatusCode) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ withStatus(status: StatusCode) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ }
+ const responseValidator = responseValidationFactory(
+ [
+ ["200", s_TodoList],
+ ["4XX", s_Error],
+ ],
+ z.undefined(),
+ )
+
+ const res = await implementation(input, responder, request)
+ .then((it) => {
+ if (it instanceof Response) {
+ return it
+ }
+ const {status, body} = it.unpack()
+ const validatedBody = responseValidator(status, body)
+
+ return validatedBody !== undefined
+ ? Response.json(validatedBody, {status})
+ : new Response(undefined, {status})
+ })
+ .catch((err) => {
+ throw OpenAPIRuntimeError.HandlerError(err)
+ })
+
+ return res
+ } catch (err) {
+ return await onError(err)
+ }
+ }
+
+const deleteTodoListByIdParamSchema = z.object({listId: z.string()})
+
+export const _DELETE =
+ (
+ implementation: DeleteTodoListById,
+ onError: (err: unknown) => Promise,
+ ) =>
+ async (
+ request: NextRequest,
+ {params}: {params: Promise},
+ ): Promise => {
+ try {
+ const input = {
+ params: parseRequestInput(
+ deleteTodoListByIdParamSchema,
+ await params,
+ RequestInputType.RouteParam,
+ ),
+ query: undefined,
+ body: undefined,
+ headers: undefined,
+ }
+ const responder = {
+ with204() {
+ return new OpenAPIRuntimeResponse(204)
+ },
+ withStatusCode4xx(status: StatusCode4xx) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ withDefault(status: StatusCode) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ withStatus(status: StatusCode) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ }
+ const responseValidator = responseValidationFactory(
+ [
+ ["204", z.undefined()],
+ ["4XX", s_Error],
+ ],
+ z.undefined(),
+ )
+
+ const res = await implementation(input, responder, request)
+ .then((it) => {
+ if (it instanceof Response) {
+ return it
+ }
+ const {status, body} = it.unpack()
+ const validatedBody = responseValidator(status, body)
+
+ return validatedBody !== undefined
+ ? Response.json(validatedBody, {status})
+ : new Response(undefined, {status})
+ })
+ .catch((err) => {
+ throw OpenAPIRuntimeError.HandlerError(err)
+ })
+
+ return res
+ } catch (err) {
+ return await onError(err)
+ }
+ }
diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts
new file mode 100644
index 000000000..315c3186c
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts
@@ -0,0 +1,104 @@
+/** AUTOGENERATED - DO NOT EDIT **/
+/* tslint:disable */
+/* eslint-disable */
+
+import {
+ OpenAPIRuntimeError,
+ RequestInputType,
+} from "@nahkies/typescript-nextjs-runtime/errors"
+import {
+ type OpenAPIRuntimeResponder,
+ OpenAPIRuntimeResponse,
+ type Params,
+ type StatusCode,
+} from "@nahkies/typescript-nextjs-runtime/server"
+import {
+ parseRequestInput,
+ responseValidationFactory,
+} from "@nahkies/typescript-nextjs-runtime/zod-v4"
+import type {NextRequest} from "next/server"
+import {z} from "zod/v4"
+import type {
+ t_GetTodoListsQuerySchema,
+ t_TodoList,
+} from "@/generated/todo-lists.yaml/models"
+import {s_Statuses, s_TodoList} from "@/generated/todo-lists.yaml/schemas"
+
+// /list
+export type GetTodoListsResponder = {
+ with200(): OpenAPIRuntimeResponse
+} & OpenAPIRuntimeResponder
+
+export type GetTodoLists = (
+ params: Params,
+ respond: GetTodoListsResponder,
+ request: NextRequest,
+) => Promise>
+
+const getTodoListsQuerySchema = z.object({
+ created: z.iso.datetime({offset: true}).optional(),
+ statuses: z
+ .preprocess(
+ (it: unknown) => (Array.isArray(it) || it === undefined ? it : [it]),
+ s_Statuses,
+ )
+ .optional(),
+ tags: z
+ .preprocess(
+ (it: unknown) => (Array.isArray(it) || it === undefined ? it : [it]),
+ z.array(z.string()),
+ )
+ .optional(),
+})
+
+export const _GET =
+ (
+ implementation: GetTodoLists,
+ onError: (err: unknown) => Promise,
+ ) =>
+ async (request: NextRequest): Promise => {
+ try {
+ const input = {
+ params: undefined,
+ query: parseRequestInput(
+ getTodoListsQuerySchema,
+ Object.fromEntries(request.nextUrl.searchParams.entries()),
+ RequestInputType.QueryString,
+ ),
+ body: undefined,
+ headers: undefined,
+ }
+ const responder = {
+ with200() {
+ return new OpenAPIRuntimeResponse(200)
+ },
+ withStatus(status: StatusCode) {
+ return new OpenAPIRuntimeResponse(status)
+ },
+ }
+ const responseValidator = responseValidationFactory(
+ [["200", z.array(s_TodoList)]],
+ undefined,
+ )
+
+ const res = await implementation(input, responder, request)
+ .then((it) => {
+ if (it instanceof Response) {
+ return it
+ }
+ const {status, body} = it.unpack()
+ const validatedBody = responseValidator(status, body)
+
+ return validatedBody !== undefined
+ ? Response.json(validatedBody, {status})
+ : new Response(undefined, {status})
+ })
+ .catch((err) => {
+ throw OpenAPIRuntimeError.HandlerError(err)
+ })
+
+ return res
+ } catch (err) {
+ return await onError(err)
+ }
+ }
diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/models.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/models.ts
new file mode 100644
index 000000000..8633abcde
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/models.ts
@@ -0,0 +1,61 @@
+/** AUTOGENERATED - DO NOT EDIT **/
+/* tslint:disable */
+/* eslint-disable */
+
+export type t_Error = {
+ code?: number
+ message?: string
+}
+
+export type t_CreateUpdateTodoList = {
+ name: string
+}
+
+export type t_Statuses = ("incomplete" | "complete")[]
+
+export type t_TodoList = {
+ created: string
+ id: string
+ incompleteItemCount: number
+ name: string
+ totalItemCount: number
+ updated: string
+}
+
+export type t_CreateTodoListItemParamSchema = {
+ listId: string
+}
+
+export type t_CreateTodoListItemRequestBody = {
+ completedAt?: string
+ content: string
+ id: string
+}
+
+export type t_DeleteTodoListByIdParamSchema = {
+ listId: string
+}
+
+export type t_UnknownObject = Record
+
+export type t_GetTodoListByIdParamSchema = {
+ listId: string
+}
+
+export type t_GetTodoListItemsParamSchema = {
+ listId: string
+}
+
+export type t_GetTodoListsQuerySchema = {
+ created?: string
+ statuses?: t_Statuses
+ tags?: string[]
+}
+
+export type t_ReplaceAttachmentParamSchema = {
+ id: string
+}
+
+export type t_UpdateTodoListByIdParamSchema = {
+ listId: string
+}
diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/schemas.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/schemas.ts
new file mode 100644
index 000000000..8c92f6005
--- /dev/null
+++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/schemas.ts
@@ -0,0 +1,31 @@
+/** AUTOGENERATED - DO NOT EDIT **/
+/* tslint:disable */
+/* eslint-disable */
+
+import {z} from "zod/v4"
+
+export const s_CreateUpdateTodoList = z.object({name: z.string()})
+
+export const s_Error = z.object({
+ message: z.string().optional(),
+ code: z.coerce.number().optional(),
+})
+
+export const s_Statuses = z.array(z.enum(["incomplete", "complete"]))
+
+export const s_TodoList = z.object({
+ id: z.string(),
+ name: z.string(),
+ totalItemCount: z.coerce.number(),
+ incompleteItemCount: z.coerce.number(),
+ created: z.iso.datetime({offset: true}),
+ updated: z.iso.datetime({offset: true}),
+})
+
+export const s_UnknownObject = z.record(z.string(), z.unknown())
+
+export const s_CreateTodoListItemRequestBody = z.object({
+ id: z.string(),
+ content: z.string(),
+ completedAt: z.iso.datetime({offset: true}).optional(),
+})
diff --git a/integration-tests/typescript-nextjs/tsconfig.json b/integration-tests/typescript-nextjs/tsconfig.json
new file mode 100644
index 000000000..7b2858930
--- /dev/null
+++ b/integration-tests/typescript-nextjs/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/documentation/src/app/guides/server-templates/typescript-nextjs/page.mdx b/packages/documentation/src/app/guides/server-templates/typescript-nextjs/page.mdx
new file mode 100644
index 000000000..836fb5cf0
--- /dev/null
+++ b/packages/documentation/src/app/guides/server-templates/typescript-nextjs/page.mdx
@@ -0,0 +1,173 @@
+import {Tabs} from 'nextra/components'
+
+# Using the `typescript-nextjs` template
+
+> ⚠️ **Alpha Template** ⚠️
+>
+> This template is currently in **alpha**. APIs and features are subject to change.
+> It might break in unexpected ways, or **mangle** your code.
+>
+> You can see an example of it used on a real project here:
+> https://github.com/mnahkies/spdx-dependency-track
+
+
+The `typescript-nextjs` template outputs scaffolding code that handles the following:
+
+- Generates route handlers in the Next.js App Router (app/api/.../route.ts) for every operation in your OpenAPI spec
+- Parses and validates request input (query, params, headers, and body) using `zod`, or `joi`
+- Validates response types at runtime before sending them, ensuring they conform to your OpenAPI spec
+- Enforces full type safety for each handler’s inputs and outputs
+- Additionally, emits a [typescript-fetch](../client-templates/typescript-fetch) client for making requests to the routes from your react code
+
+See [integration-tests/typescript-nextjs](https://github.com/mnahkies/openapi-code-generator/tree/main/integration-tests/typescript-nextjs) for more samples.
+
+### Install dependencies
+First install the CLI and the required runtime packages to your project:
+```sh npm2yarn
+npm i --dev @nahkies/openapi-code-generator
+npm i @nahkies/typescript-nextjs-runtime next zod
+```
+
+See also [quick start](../../getting-started/quick-start) guide
+
+### Run generation
+
+
+
+ ```sh npm2yarn
+ npm run openapi-code-generator \
+ --input ./openapi.yaml \
+ --input-type openapi3 \
+ --output ./src \
+ --template typescript-nextjs \
+ --schema-builder zod
+ ```
+
+
+ ```sh npm2yarn
+ npm run openapi-code-generator \
+ --input ./typespec.tsp \
+ --input-type typespec \
+ --output ./src \
+ --template typescript-nextjs \
+ --schema-builder zod
+ ```
+
+
+
+
+### Using the generated code
+Running the above will output a bunch of files into `./src`. Here's an example of the files output for a todo-list api specification:
+```shell
+src
+├── app
+│ ├── api
+│ │ └── list
+│ │ ├── [listId]
+│ │ │ ├── items
+│ │ │ │ └── route.ts
+│ │ │ └── route.ts
+│ │ └── route.ts
+│ ├── layout.tsx
+│ └── page.tsx
+└── generated
+ ├── api
+ │ └── list
+ │ ├── [listId]
+ │ │ ├── items
+ │ │ │ └── route.ts
+ │ │ └── route.ts
+ │ └── route.ts
+ ├── client.ts
+ ├── models.ts
+ └── schemas.ts
+````
+
+`./src/app/../route.ts`
+- a `route.ts` file is generated per operation, following Next.js App Router conventions
+- exports handlers (GET, POST, etc.) for each HTTP method defined
+- safe to edit, your route handler implementations go here
+- calls into `./src/generated/...` for input/output validation logic
+
+`./src/generated/../route.ts`
+- mirror structure of the `./src/app/.../route.ts` files
+- contains glue code that parses input, validates responses, and calls your implementation
+
+`./src/generated/models.ts`
+- exports plain TypeScript types for all schemas in your OpenAPI spec
+
+`./src/generated/schemas.ts`
+- exports runtime schema validators (`zod` / `joi` depending on configuration)
+
+`./src/generated/client.ts`
+- exports a [typescript-fetch](../client-templates/typescript-fetch) client for calling your API from frontend code
+- see [use-with-react-query](../use-with-react-query) for integration with `react-query`
+
+#### Implementing routes
+
+Once generated usage should look something like this:
+
+```typescript
+import {db} from "../../../../../db"
+import {
+ _GET,
+ _POST,
+} from "../../../../../generated/todo-lists.yaml/list/[listId]/items/route"
+
+export const GET = _GET(async ({params}, respond, request) => {
+ const items = db.getTodoItems({listId: params.listId})
+
+ if (items) {
+ return respond.with200().body(items)
+ }
+ return respond
+ .with404()
+ .body({code: "not-found", message: `listId ${params.listId} not found`})
+})
+
+export const POST = _POST(async ({params, body}, respond, request) => {
+ await db.insertTodoItem({
+ listId: params.listId,
+ itemId: body.id,
+ content: body.content,
+ completedAt: body.completedAt,
+ })
+
+ return respond.with204()
+})
+
+```
+
+#### Its safe to regenerate!
+The template uses [ts-morph](https://ts-morph.com/) to **non-destructively generate and update** route.ts files.
+
+This means you can safely add your own logic to the scaffolded files, and future regenerations will preserve your
+implementation code while updating the generated boilerplate.
+
+#### Error Handling
+
+> 🚧 Under construction
+>
+> Errors will be thrown for req/res validation issues, but currently its impossible to catch them.
+> More thought is needed...
+
+### Escape Hatches
+
+> 🚧 Under construction
+>
+> The raw nextjs `request` object is passed to your implementation, however there is not yet
+> a way to skip response processing.
+
+Most APIs won't need this, but in some cases (e.g. unsupported features), you can use escape hatches to drop out of
+the generated scaffolding.
+
+For example, we pass the raw nextjs `request` object to your handler implementations,
+allowing you full control where its needed.
+```typescript
+export const GET = _GET(async ({params}, respond, request) => {
+ console.log(request.nextUrl.buildId)
+ // ...your implementation here
+})
+```
+
+Use sparingly - the goal is to reduce the need for escape hatches over time.
diff --git a/packages/documentation/src/lib/playground/load-runtime-types.tsx b/packages/documentation/src/lib/playground/load-runtime-types.tsx
index 57cfd3310..5e8a14475 100644
--- a/packages/documentation/src/lib/playground/load-runtime-types.tsx
+++ b/packages/documentation/src/lib/playground/load-runtime-types.tsx
@@ -20,7 +20,8 @@ export const loadRuntimeTypes = async (
| "typescript-fetch"
| "typescript-axios"
| "typescript-koa"
- | "typescript-express",
+ | "typescript-express"
+ | "typescript-nextjs",
) => {
const fileRootPath = "file:///"
@@ -119,6 +120,29 @@ export const loadRuntimeTypes = async (
path: "/node_modules/@nahkies/typescript-express-runtime/joi.d.mts",
},
],
+ // TODO: adjust
+ "typescript-nextjs": [
+ {
+ uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/package.json",
+ path: "/node_modules/@nahkies/typescript-koa-runtime/package.json",
+ },
+ {
+ uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/dist/server.d.ts",
+ path: "/node_modules/@nahkies/typescript-koa-runtime/server.d.ts",
+ },
+ {
+ uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/dist/errors.d.ts",
+ path: "/node_modules/@nahkies/typescript-koa-runtime/errors.d.ts",
+ },
+ {
+ uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/dist/zod.d.ts",
+ path: "/node_modules/@nahkies/typescript-koa-runtime/zod.d.ts",
+ },
+ {
+ uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/dist/joi.d.ts",
+ path: "/node_modules/@nahkies/typescript-koa-runtime/joi.d.ts",
+ },
+ ],
}
for (const file of files[template]) {
diff --git a/packages/openapi-code-generator/package.json b/packages/openapi-code-generator/package.json
index 802f9066b..1809eb034 100644
--- a/packages/openapi-code-generator/package.json
+++ b/packages/openapi-code-generator/package.json
@@ -69,11 +69,11 @@
"@nahkies/typescript-common-runtime": "workspace:^",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.24",
- "@typespec/compiler": "^1.12.0",
+ "@typespec/compiler": "^1.13.0",
"@typespec/events": "0.83.0",
- "@typespec/http": "^1.12.0",
- "@typespec/openapi": "^1.12.0",
- "@typespec/openapi3": "^1.12.0",
+ "@typespec/http": "^1.13.0",
+ "@typespec/openapi": "^1.13.0",
+ "@typespec/openapi3": "^1.13.0",
"@typespec/rest": "0.83.0",
"@typespec/sse": "0.83.0",
"@typespec/streams": "0.83.0",
@@ -95,6 +95,7 @@
"js-yaml": "catalog:",
"json5": "^2.2.3",
"lodash": "^4.18.1",
+ "ts-morph": "^26.0.0",
"tslib": "^2.8.1",
"typescript": "catalog:typescript6",
"zod": "^4.4.3"
diff --git a/packages/openapi-code-generator/src/config.ts b/packages/openapi-code-generator/src/config.ts
index 1123b81dd..01edf7c4e 100644
--- a/packages/openapi-code-generator/src/config.ts
+++ b/packages/openapi-code-generator/src/config.ts
@@ -17,6 +17,7 @@ export type Config = {
| "typescript-angular"
| "typescript-koa"
| "typescript-express"
+ | "typescript-nextjs"
schemaBuilder: "zod-v3" | "zod-v4" | "joi"
enableRuntimeResponseValidation: boolean
enableTypedBasePaths: boolean
diff --git a/packages/openapi-code-generator/src/core/input.ts b/packages/openapi-code-generator/src/core/input.ts
index b7d767458..567a31458 100644
--- a/packages/openapi-code-generator/src/core/input.ts
+++ b/packages/openapi-code-generator/src/core/input.ts
@@ -30,7 +30,11 @@ import {
import {camelCase, coalesce, isDefined, isHttpMethod} from "./utils.ts"
export type OperationGroup = {name: string; operations: IROperation[]}
-export type OperationGroupStrategy = "none" | "first-tag" | "first-slug"
+export type OperationGroupStrategy =
+ | "none"
+ | "first-tag"
+ | "first-slug"
+ | "route"
export type InputConfig = {
extractInlineSchemas: boolean
@@ -45,7 +49,7 @@ export interface ISchemaProvider {
export class Input implements ISchemaProvider {
constructor(
- private loader: OpenapiLoader,
+ public loader: OpenapiLoader,
readonly config: InputConfig,
private readonly syntheticNameGenerator: SyntheticNameGenerator = defaultSyntheticNameGenerator,
private readonly schemaNormalizer = new SchemaNormalizer(config, this),
@@ -98,6 +102,8 @@ export class Input implements ISchemaProvider {
return this.operationsByFirstTag()
case "first-slug":
return this.operationsByFirstSlug()
+ case "route":
+ return this.operationsByRoute()
default:
throw new Error(`unsupported grouping strategy '${strategy}'`)
}
@@ -205,6 +211,12 @@ export class Input implements ISchemaProvider {
})
}
+ private operationsByRoute(): OperationGroup[] {
+ return this.groupOperations((operation) => {
+ return operation.route
+ })
+ }
+
private groupOperations(
groupBy: (operation: IROperation) => string | undefined,
): OperationGroup[] {
diff --git a/packages/openapi-code-generator/src/core/loaders/openapi-loader.ts b/packages/openapi-code-generator/src/core/loaders/openapi-loader.ts
index 9c216e3cf..982fa78cd 100644
--- a/packages/openapi-code-generator/src/core/loaders/openapi-loader.ts
+++ b/packages/openapi-code-generator/src/core/loaders/openapi-loader.ts
@@ -23,7 +23,7 @@ export class OpenapiLoader {
private readonly library = new Map()
private constructor(
- private readonly entryPointKey: string,
+ public readonly entryPointKey: string,
private readonly config: {titleOverride: string | undefined},
private readonly validator: IOpenapiValidator,
private readonly genericLoader: GenericLoader,
diff --git a/packages/openapi-code-generator/src/core/utils.ts b/packages/openapi-code-generator/src/core/utils.ts
index d6029a95d..c92a3bdd9 100644
--- a/packages/openapi-code-generator/src/core/utils.ts
+++ b/packages/openapi-code-generator/src/core/utils.ts
@@ -5,6 +5,10 @@ export function isDefined(it: T | undefined): it is T {
return it !== undefined
}
+export function isTruthy(it: T | undefined | null | "" | 0): it is T {
+ return Boolean(it)
+}
+
export function hasSingleElement(it: T[]): it is [T] {
return it.length === 1
}
diff --git a/packages/openapi-code-generator/src/generate.ts b/packages/openapi-code-generator/src/generate.ts
index 9cf1db502..b2c300d76 100644
--- a/packages/openapi-code-generator/src/generate.ts
+++ b/packages/openapi-code-generator/src/generate.ts
@@ -84,6 +84,7 @@ export async function generate(
await generator.run({
input,
emitter,
+ fsAdaptor,
schemaBuilder: config.schemaBuilder,
enableRuntimeResponseValidation: config.enableRuntimeResponseValidation,
enableTypedBasePaths: config.enableTypedBasePaths,
diff --git a/packages/openapi-code-generator/src/templates.ts b/packages/openapi-code-generator/src/templates.ts
index 83183ca4e..fc2a291ab 100644
--- a/packages/openapi-code-generator/src/templates.ts
+++ b/packages/openapi-code-generator/src/templates.ts
@@ -5,6 +5,7 @@ import {generateTypescriptAxios} from "./typescript/client/typescript-axios/type
import {generateTypescriptFetch} from "./typescript/client/typescript-fetch/typescript-fetch.generator.ts"
import {generateTypescriptExpress} from "./typescript/server/typescript-express/typescript-express.generator.ts"
import {generateTypescriptKoa} from "./typescript/server/typescript-koa/typescript-koa.generator.ts"
+import {generateTypescriptNextJS} from "./typescript/server/typescript-nextjs/typescript-nextjs.generator.ts"
export const templates = {
"typescript-fetch": {
@@ -37,6 +38,12 @@ export const templates = {
run: generateTypescriptExpress,
syntheticNameGenerator: defaultSyntheticNameGenerator,
},
+ "typescript-nextjs": {
+ language: "typescript",
+ type: "server",
+ run: generateTypescriptNextJS,
+ syntheticNameGenerator: defaultSyntheticNameGenerator,
+ },
} satisfies {[key: string]: OpenapiGenerator}
export const templateNames = [
@@ -45,4 +52,5 @@ export const templateNames = [
"typescript-angular",
"typescript-koa",
"typescript-express",
+ "typescript-nextjs",
] as const satisfies Array
diff --git a/packages/openapi-code-generator/src/templates.types.ts b/packages/openapi-code-generator/src/templates.types.ts
index 892fde2b1..4cea958ea 100644
--- a/packages/openapi-code-generator/src/templates.types.ts
+++ b/packages/openapi-code-generator/src/templates.types.ts
@@ -1,3 +1,4 @@
+import type {IFsAdaptor} from "./core/file-system/fs-adaptor.ts"
import type {Input, OperationGroupStrategy} from "./core/input.ts"
import type {CompilerOptions} from "./core/loaders/tsconfig.loader.ts"
import type {SyntheticNameGenerator} from "./core/synthetic-name-generator.ts"
@@ -21,6 +22,7 @@ export interface OpenapiGeneratorConfig {
enableTypedBasePaths: boolean
groupingStrategy: OperationGroupStrategy
filenameConvention: IdentifierConvention
+ fsAdaptor: IFsAdaptor
}
export interface OpenapiTypescriptGeneratorConfig
diff --git a/packages/openapi-code-generator/src/typescript/common/compilation-units.ts b/packages/openapi-code-generator/src/typescript/common/compilation-units.ts
index d3fa04adc..6239df1ff 100644
--- a/packages/openapi-code-generator/src/typescript/common/compilation-units.ts
+++ b/packages/openapi-code-generator/src/typescript/common/compilation-units.ts
@@ -15,6 +15,7 @@ export class CompilationUnit {
readonly filename: string,
readonly imports: ImportBuilder | undefined,
readonly code: string,
+ readonly isAutogenerated: boolean = true,
) {}
hasCode() {
@@ -29,7 +30,7 @@ export class CompilationUnit {
includeHeader?: boolean
}): string {
return [
- includeHeader ? FILE_HEADER : "",
+ includeHeader && this.isAutogenerated ? FILE_HEADER : "",
this.imports
? `${this.imports.toString(allowUnusedImports ? "" : this.code)}\n`
: "",
diff --git a/packages/openapi-code-generator/src/typescript/common/import-builder.ts b/packages/openapi-code-generator/src/typescript/common/import-builder.ts
index 1566131c4..c4377b384 100644
--- a/packages/openapi-code-generator/src/typescript/common/import-builder.ts
+++ b/packages/openapi-code-generator/src/typescript/common/import-builder.ts
@@ -89,6 +89,7 @@ export function categorizeImportSource(source: string): ImportCategory {
export type ImportBuilderConfig = {
unit?: {filename: string} | undefined
+ importAlias?: string | undefined
includeFileExtensions: boolean
}
@@ -203,7 +204,7 @@ export class ImportBuilder {
isType: boolean,
): void {
// biome-ignore lint/style/noParameterAssign: normalization
- from = this.normalizeFrom(from)
+ from = this.normalizeFrom(from, this.config.unit?.filename)
if (!this.imports[from]) {
this.imports[from] = {
@@ -233,7 +234,7 @@ export class ImportBuilder {
}
}
- private normalizeFrom(from: string) {
+ public normalizeFrom(from: string, filename?: string) {
if (!this.config.includeFileExtensions && from.endsWith(".ts")) {
// biome-ignore lint/style/noParameterAssign: normalization
from = from.substring(0, from.length - ".ts".length)
@@ -243,20 +244,29 @@ export class ImportBuilder {
from = normalizeToUnix(from)
}
- if (this.config.unit) {
- const unitFilename = this.config.unit.filename
+ const unitFilename = filename ?? this.config.unit?.filename
+
+ if (unitFilename) {
+ const normalizedUnitFilename = normalizeToUnix(unitFilename)
const isFromPath =
from.startsWith("./") ||
from.startsWith("../") ||
path.posix.isAbsolute(from)
if (isFromPath) {
- const root = path.posix.isAbsolute(unitFilename)
- ? path.posix.parse(unitFilename).root
+ if (this.config.importAlias) {
+ return (
+ this.config.importAlias +
+ from.split(path.posix.sep).slice(1).join(path.posix.sep)
+ )
+ }
+
+ const root = path.posix.isAbsolute(normalizedUnitFilename)
+ ? path.posix.parse(normalizedUnitFilename).root
: "/"
const unitDir = path.posix.dirname(
- path.posix.resolve(root, unitFilename),
+ path.posix.resolve(root, normalizedUnitFilename),
)
const fromDir = path.posix.dirname(path.posix.resolve(root, from))
diff --git a/packages/openapi-code-generator/src/typescript/common/typescript-emitter.ts b/packages/openapi-code-generator/src/typescript/common/typescript-emitter.ts
index b5867658e..d12d53f61 100644
--- a/packages/openapi-code-generator/src/typescript/common/typescript-emitter.ts
+++ b/packages/openapi-code-generator/src/typescript/common/typescript-emitter.ts
@@ -8,7 +8,7 @@ export class TypescriptEmitter {
constructor(
private readonly fsAdaptor: IFsAdaptor,
private readonly formatter: IFormatter,
- private readonly config: {
+ public readonly config: {
destinationDirectory: string
allowUnusedImports: boolean
},
diff --git a/packages/openapi-code-generator/src/typescript/server/abstract-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/abstract-router-builder.ts
index 30ad59b1e..59d93ab56 100644
--- a/packages/openapi-code-generator/src/typescript/server/abstract-router-builder.ts
+++ b/packages/openapi-code-generator/src/typescript/server/abstract-router-builder.ts
@@ -17,6 +17,8 @@ export abstract class AbstractRouterBuilder implements ICompilable {
"application/scim+json",
"application/merge-patch+json",
"application/x-www-form-urlencoded",
+ // todo:
+ // "multipart/form-data",
"application/octet-stream",
"text/json",
"text/plain",
diff --git a/packages/openapi-code-generator/src/typescript/server/server-operation-builder.ts b/packages/openapi-code-generator/src/typescript/server/server-operation-builder.ts
index f07e6815f..95996a47b 100644
--- a/packages/openapi-code-generator/src/typescript/server/server-operation-builder.ts
+++ b/packages/openapi-code-generator/src/typescript/server/server-operation-builder.ts
@@ -40,6 +40,7 @@ export type ServerOperationResponseSchemas = {
}
export type Parameters = {
+ hasParams: boolean
type: string
path: {
name: string
@@ -126,7 +127,16 @@ export class ServerOperationBuilder {
${header.type}
>`
- return {type, path, query, header, body}
+ return {
+ hasParams: Boolean(
+ path.schema || query.schema || header.schema || body.schema,
+ ),
+ type,
+ path,
+ query,
+ header,
+ body,
+ }
}
responseValidator(): string {
diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts
new file mode 100644
index 000000000..6a072c089
--- /dev/null
+++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts
@@ -0,0 +1,212 @@
+import {
+ type SourceFile,
+ StructureKind,
+ ts,
+ VariableDeclarationKind,
+} from "ts-morph"
+import type {Input} from "../../../core/input.ts"
+import type {IROperation} from "../../../core/openapi-types-normalized.ts"
+import {
+ type HttpMethod,
+ isDefined,
+ isHttpMethod,
+ titleCase,
+} from "../../../core/utils.ts"
+import {
+ CompilationUnit,
+ type ICompilable,
+} from "../../common/compilation-units.ts"
+import type {ImportBuilder} from "../../common/import-builder.ts"
+import type {SchemaBuilder} from "../../common/schema-builders/schema-builder.ts"
+import type {TypeBuilder} from "../../common/type-builder/type-builder.ts"
+import {
+ ServerOperationBuilder,
+ type ServerSymbols,
+} from "../server-operation-builder.ts"
+
+import SyntaxKind = ts.SyntaxKind
+
+export class TypescriptNextjsAppRouterBuilder implements ICompilable {
+ protected readonly capabilities = {
+ requestBody: {
+ mediaTypes: [
+ "application/json",
+ "application/scim+json",
+ "application/merge-patch+json",
+ "application/x-www-form-urlencoded",
+ "application/octet-stream",
+ "text/json",
+ "text/plain",
+ "text/x-markdown",
+ ],
+ },
+ }
+
+ constructor(
+ private readonly filename: string,
+ readonly name: string,
+ private readonly input: Input,
+ private readonly imports: ImportBuilder,
+ private readonly types: TypeBuilder,
+ private readonly schemaBuilder: SchemaBuilder,
+ private readonly companionFilename: string,
+ private readonly sourceFile: SourceFile,
+ ) {}
+
+ private readonly httpMethodsUsed = new Set()
+
+ add(operation: IROperation): void {
+ const builder = new ServerOperationBuilder(
+ operation,
+ this.input,
+ this.types,
+ this.schemaBuilder,
+ {
+ requestBody: {
+ supportedMediaTypes: this.capabilities.requestBody.mediaTypes,
+ defaultMaxSize: "10mb",
+ },
+ },
+ )
+
+ const params = builder.parameters()
+
+ const sourceFile = this.sourceFile
+
+ const wrappingMethod = `_${operation.method.toUpperCase()}`
+
+ this.httpMethodsUsed.add(operation.method)
+
+ // Get the existing function, or create a new default one
+ const variableDeclaration =
+ sourceFile
+ .getVariableDeclaration(operation.method.toUpperCase())
+ ?.getVariableStatement() ||
+ sourceFile.addVariableStatement({
+ isExported: true,
+ declarationKind: VariableDeclarationKind.Const,
+ declarations: [
+ {
+ name: operation.method.toUpperCase(),
+ kind: StructureKind.VariableDeclaration,
+ initializer: `${wrappingMethod}(async (input, respond, context) => {
+ // TODO: implementation
+ return respond.withStatus(501).body({message: "not implemented"} as any)
+ }, async (err) => {
+ // TODO: implementation
+ return new Response(JSON.stringify({message: "not implemented"}), {status: 501})
+ })`,
+ },
+ ],
+ })
+
+ // Replace the params based on what inputs we have
+ // biome-ignore lint/style/noNonNullAssertion: todo
+ const declarations = variableDeclaration.getDeclarations()[0]!
+ const callExpression = declarations.getInitializerIfKindOrThrow(
+ SyntaxKind.CallExpression,
+ )
+
+ // biome-ignore lint/style/noNonNullAssertion: todo
+ const implementationFunction = callExpression
+ .getArguments()[0]!
+ .asKind(SyntaxKind.ArrowFunction)!
+
+ // biome-ignore lint/complexity/noForEach: todo
+ implementationFunction?.getParameters().forEach((parameter) => {
+ parameter.remove()
+ })
+
+ if (params.hasParams) {
+ implementationFunction?.addParameter({
+ name: `{${[
+ params.path.schema ? "params" : undefined,
+ params.query.schema ? "query" : undefined,
+ params.body.schema ? "body" : undefined,
+ params.header.schema ? "headers" : undefined,
+ ]
+ .filter(isDefined)
+ .join(",")}}`,
+ })
+ }
+
+ implementationFunction?.addParameter({name: "respond"})
+ implementationFunction?.addParameter({name: "request"})
+
+ const onErrorFunction = callExpression.getArguments()[1]
+
+ if (!onErrorFunction) {
+ callExpression.addArgument(`async (err) => {
+ // TODO: implementation
+ return new Response(JSON.stringify({message: "not implemented"}), {status: 501})
+ }`)
+ } else if (onErrorFunction.getKind() === SyntaxKind.ArrowFunction) {
+ for (const param of onErrorFunction
+ .asKind(SyntaxKind.ArrowFunction)
+ ?.getParameters() ?? []) {
+ param.remove()
+ }
+
+ onErrorFunction
+ ?.asKind(SyntaxKind.ArrowFunction)
+ ?.addParameter({name: "err"})
+ } else if (onErrorFunction.getKind() === SyntaxKind.FunctionExpression) {
+ // todo
+ }
+ }
+
+ // TODO: duplication - should be shared with router builder
+ protected operationSymbols(operationId: string): ServerSymbols {
+ return {
+ implPropName: operationId,
+ implTypeName: titleCase(operationId),
+ responderName: `${titleCase(operationId)}Responder`,
+ responseBodyValidator: `${operationId}ResponseValidator`,
+ }
+ }
+
+ toString(): string {
+ return this.sourceFile.getFullText()
+ }
+
+ toCompilationUnit(): CompilationUnit {
+ // Reconcile imports - attempt to find an existing one and replace it with correct one
+ const imports = this.sourceFile.getImportDeclarations()
+ const from = this.imports.normalizeFrom(
+ `./${this.companionFilename}`,
+ `./${this.filename}`,
+ )
+ // biome-ignore lint/complexity/noForEach: todo
+ imports
+ .filter((it) => it.getModuleSpecifierValue().includes(from))
+ .forEach((it) => {
+ it.remove()
+ })
+
+ this.sourceFile.addImportDeclaration({
+ namedImports: Array.from(this.httpMethodsUsed)
+ .map((it) => `_${it}`)
+ .sort(),
+ moduleSpecifier: from,
+ })
+
+ // Remove any methods that were removed from the spec
+ // biome-ignore lint/complexity/noForEach: todo
+ this.sourceFile
+ .getVariableDeclarations()
+ .filter((it) => {
+ const name = it.getName()
+ return isHttpMethod(name) && !this.httpMethodsUsed.has(name)
+ })
+ .forEach((it) => {
+ it.remove()
+ })
+
+ return new CompilationUnit(
+ this.filename,
+ this.imports,
+ this.toString(),
+ false,
+ )
+ }
+}
diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts
new file mode 100644
index 000000000..5e9eff7d3
--- /dev/null
+++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts
@@ -0,0 +1,218 @@
+import type {Input} from "../../../core/input.ts"
+import {isDefined, titleCase} from "../../../core/utils.ts"
+import type {ImportBuilder} from "../../common/import-builder.ts"
+import type {SchemaBuilder} from "../../common/schema-builders/schema-builder.ts"
+import type {TypeBuilder} from "../../common/type-builder/type-builder.ts"
+import {constStatement, object} from "../../common/type-utils.ts"
+import {buildExport} from "../../common/typescript-common.ts"
+import {AbstractRouterBuilder} from "../abstract-router-builder.ts"
+import type {
+ ServerOperationBuilder,
+ ServerSymbols,
+} from "../server-operation-builder.ts"
+
+export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder {
+ private readonly operationTypes: {
+ operationId: string
+ statements: string[]
+ }[] = []
+
+ // biome-ignore lint/complexity/noUselessConstructor: todo
+ constructor(
+ filename: string,
+ name: string,
+ input: Input,
+ imports: ImportBuilder,
+ types: TypeBuilder,
+ schemaBuilder: SchemaBuilder,
+ ) {
+ super(filename, name, input, imports, types, schemaBuilder)
+ }
+
+ protected buildImports(): void {
+ this.imports
+ .from("@nahkies/typescript-nextjs-runtime/server")
+ .add("OpenAPIRuntimeResponse", "parseQueryParameters")
+ .addType(
+ "OpenAPIRuntimeResponder",
+ "Params",
+ "StatusCode2xx",
+ "StatusCode3xx",
+ "StatusCode4xx",
+ "StatusCode5xx",
+ "StatusCode",
+ )
+
+ this.imports.from("next/server").addType("NextRequest", "NextResponse")
+
+ this.imports
+ .from("@nahkies/typescript-nextjs-runtime/errors")
+ .add("OpenAPIRuntimeError", "RequestInputType")
+
+ const schemaBuilderType = this.schemaBuilder.type
+
+ switch (schemaBuilderType) {
+ case "joi": {
+ this.imports
+ .from("@nahkies/typescript-nextjs-runtime/joi")
+ .add("parseRequestInput", "responseValidationFactory")
+ break
+ }
+ case "zod-v3": {
+ this.imports
+ .from("@nahkies/typescript-nextjs-runtime/zod-v3")
+ .add("parseRequestInput", "responseValidationFactory")
+ break
+ }
+ case "zod-v4": {
+ this.imports
+ .from("@nahkies/typescript-nextjs-runtime/zod-v4")
+ .add("parseRequestInput", "responseValidationFactory")
+ break
+ }
+ default: {
+ throw new Error(
+ `unsupported schema builder type '${schemaBuilderType satisfies never}'`,
+ )
+ }
+ }
+ }
+
+ protected buildOperation(builder: ServerOperationBuilder): string {
+ const statements: string[] = []
+
+ const symbols = this.operationSymbols(builder.operationId)
+ const params = builder.parameters()
+
+ if (params.path.schema) {
+ statements.push(constStatement(params.path.name, params.path.schema))
+ }
+
+ if (params.query.schema) {
+ statements.push(constStatement(params.query.name, params.query.schema))
+ }
+
+ if (params.header.schema) {
+ statements.push(constStatement(params.header.name, params.header.schema))
+ }
+
+ const responder = builder.responder(
+ "OpenAPIRuntimeResponder",
+ "OpenAPIRuntimeResponse",
+ )
+
+ this.operationTypes.push({
+ operationId: builder.operationId,
+ statements: [
+ buildExport({
+ name: symbols.responderName,
+ value: responder.type,
+ kind: "type",
+ }),
+ buildExport({
+ name: symbols.implTypeName,
+ value: `(${[
+ params.hasParams ? `params: ${params.type}` : undefined,
+ `respond: ${symbols.responderName}`,
+ "request: NextRequest",
+ ]
+ .filter(isDefined)
+ .join(",")}) => Promise>`,
+ kind: "type",
+ }),
+ ],
+ })
+
+ const inputObject = object([
+ this.parseRequestInput("params", {
+ name: params.path.name,
+ schema: params.path.schema,
+ source: "await params",
+ type: "RequestInputType.RouteParam",
+ }),
+ this.parseRequestInput("query", {
+ name: params.query.name,
+ schema: params.query.schema,
+ source: params.query.isSimpleQuery
+ ? "Object.fromEntries(request.nextUrl.searchParams.entries())"
+ : `parseQueryParameters(request.nextUrl.search, ${JSON.stringify(params.query.parameters)})`,
+ type: "RequestInputType.QueryString",
+ }),
+ this.parseRequestInput("body", {
+ name: params.body.schema,
+ schema: params.body.schema,
+ source:
+ params.body.contentType === "application/octet-stream"
+ ? "await request.blob()"
+ : params.body.contentType === "application/x-www-form-urlencoded" ||
+ params.body.contentType === "multipart/form-data"
+ ? "await request.formData()"
+ : "await request.json()",
+ type: "RequestInputType.RequestBody",
+ comment:
+ params.body.schema && !params.body.isSupported
+ ? `// todo: request bodies with content-type '${params.body.contentType}' not yet supported`
+ : "",
+ }) + (params.body.schema && !params.body.isSupported ? " as never" : ""),
+ this.parseRequestInput("headers", {
+ name: params.header.name,
+ schema: params.header.schema,
+ source: 'Reflect.get(request, "headers")',
+ type: "RequestInputType.RequestHeader",
+ }),
+ ])
+
+ statements.push(
+ buildExport({
+ name: `_${builder.method.toUpperCase()}`,
+ kind: "const",
+ value: `(implementation: ${symbols.implTypeName}, onError: (err: unknown) => Promise) => async (${["request: NextRequest", params.path.schema ? "{params}: {params: Promise}" : undefined].filter(isDefined).join(",")}): Promise => {
+try {
+ ${params.hasParams ? `const input = ${inputObject}` : ""}
+ const responder = ${responder.implementation}
+ const responseValidator = ${builder.responseValidator()}
+
+ const res = await implementation(${[params.hasParams ? "input" : undefined, "responder", "request"].filter(isDefined).join(",")})
+ .then(it => {
+ if(it instanceof Response) {
+ return it
+ }
+ const {status, body} = it.unpack()
+ const validatedBody = responseValidator(status, body)
+
+ return validatedBody !== undefined ? Response.json(validatedBody, {status}) : new Response(undefined, {status})
+ })
+ .catch(err => { throw OpenAPIRuntimeError.HandlerError(err) })
+
+ return res
+ } catch (err) {
+ return await onError(err)
+ }
+ }`,
+ }),
+ )
+
+ return statements.join("\n\n")
+ }
+
+ protected operationSymbols(operationId: string): ServerSymbols {
+ return {
+ implPropName: operationId,
+ implTypeName: titleCase(operationId),
+ responderName: `${titleCase(operationId)}Responder`,
+ responseBodyValidator: `${operationId}ResponseValidator`,
+ }
+ }
+
+ protected buildRouter(
+ routerName: string,
+ routerStatements: string[],
+ ): string {
+ return `
+// ${routerName}
+${this.operationTypes.flatMap((it) => it.statements).join("\n\n")}
+
+${routerStatements.join("\n\n")}
+`
+ }
+}
diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts
new file mode 100644
index 000000000..0685f18aa
--- /dev/null
+++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts
@@ -0,0 +1,196 @@
+// biome-ignore lint/style/useNodejsImportProtocol: todo
+import path from "path"
+import {Project, type SourceFile} from "ts-morph"
+import type {IFsAdaptor} from "../../../core/file-system/fs-adaptor.ts"
+import type {CompilerOptions} from "../../../core/loaders/tsconfig.loader.ts"
+import {isTruthy} from "../../../core/utils.ts"
+import type {OpenapiTypescriptGeneratorConfig} from "../../../templates.types.ts"
+import {TypescriptFetchClientBuilder} from "../../client/typescript-fetch/typescript-fetch-client-builder.ts"
+import {ImportBuilder} from "../../common/import-builder.ts"
+import {schemaBuilderFactory} from "../../common/schema-builders/schema-builder.ts"
+import {TypeBuilder} from "../../common/type-builder/type-builder.ts"
+import {TypescriptNextjsAppRouterBuilder} from "./typescript-nextjs-app-router-builder.ts"
+import {TypescriptNextjsRouterBuilder} from "./typescript-nextjs-router-builder.ts"
+
+function findImportAlias(dest: string, compilerOptions: CompilerOptions) {
+ const relative = `./${path.relative(process.cwd(), dest)}/*`
+
+ const alias = Object.entries(compilerOptions.paths || {}).find(([, paths]) =>
+ paths.some((p) => p === relative || relative.endsWith(p.substring(1))),
+ )
+
+ return alias ? alias[0].replace("*", "") : undefined
+}
+
+export async function generateTypescriptNextJS(
+ config: OpenapiTypescriptGeneratorConfig,
+): Promise {
+ const {input, emitter, allowAny} = config
+
+ const importAlias = findImportAlias(
+ config.emitter.config.destinationDirectory,
+ config.compilerOptions,
+ )
+
+ // biome-ignore lint/complexity/useLiteralKeys: todo
+ const subDirectory = process.env["OPENAPI_INTEGRATION_TESTS"]
+ ? path.basename(config.input.loader.entryPointKey)
+ : ""
+
+ const appDirectory = [".", "app", subDirectory]
+ .filter(isTruthy)
+ .join(path.sep)
+
+ const generatedDirectory = [".", "generated", subDirectory]
+ .filter(isTruthy)
+ .join(path.sep)
+
+ const importBuilderConfig = {includeFileExtensions: false}
+ const schemaBuilderImports = new ImportBuilder(importBuilderConfig)
+
+ const rootTypeBuilder = await TypeBuilder.fromSchemaProvider(
+ [generatedDirectory, "models.ts"].join(path.sep),
+ input,
+ config.compilerOptions,
+ {allowAny},
+ )
+
+ const rootSchemaBuilder = await schemaBuilderFactory(
+ [generatedDirectory, "schemas.ts"].join(path.sep),
+ input,
+ config.schemaBuilder,
+ {allowAny},
+ schemaBuilderImports,
+ rootTypeBuilder,
+ )
+
+ const project = new Project({useInMemoryFileSystem: true})
+
+ const serverRouters = (
+ await Promise.all(
+ input.groupedOperations("route").map(async (group) => {
+ const filename = path.join(
+ generatedDirectory,
+ routeToNextJSFilepath(group.name),
+ )
+
+ const routerImports = new ImportBuilder({
+ unit: {filename},
+ includeFileExtensions: false,
+ importAlias,
+ })
+
+ const routerBuilder = new TypescriptNextjsRouterBuilder(
+ filename,
+ group.name,
+ input,
+ routerImports,
+ rootTypeBuilder.withImports(routerImports),
+ rootSchemaBuilder.withImports(routerImports),
+ )
+
+ const nextJsAppRouterPath = path.join(
+ appDirectory,
+ routeToNextJSFilepath(group.name),
+ )
+
+ const appRouterImports = new ImportBuilder({
+ unit: {filename: nextJsAppRouterPath},
+ includeFileExtensions: false,
+ importAlias,
+ })
+
+ const sourceFile = await loadExistingRouteImplementation({
+ fsAdaptor: config.fsAdaptor,
+ project,
+ destinationDirectory: emitter.config.destinationDirectory,
+ nextJsAppRouterPath,
+ })
+
+ const nextJSAppRouterBuilder = new TypescriptNextjsAppRouterBuilder(
+ nextJsAppRouterPath,
+ group.name,
+ input,
+ appRouterImports,
+ rootTypeBuilder.withImports(appRouterImports),
+ rootSchemaBuilder.withImports(appRouterImports),
+ filename,
+ sourceFile,
+ )
+
+ for (const operation of group.operations) {
+ routerBuilder.add(operation)
+ nextJSAppRouterBuilder.add(operation)
+ }
+
+ return [
+ routerBuilder.toCompilationUnit(),
+ nextJSAppRouterBuilder.toCompilationUnit(),
+ ]
+ }),
+ )
+ ).flat()
+
+ const clientOutputPath = [generatedDirectory, "client.ts"].join(path.sep)
+ const clientImportBuilder = new ImportBuilder({
+ unit: {filename: clientOutputPath},
+ importAlias,
+ includeFileExtensions: false,
+ })
+
+ const fetchClientBuilder = new TypescriptFetchClientBuilder(
+ clientOutputPath,
+ "ApiClient",
+ input,
+ clientImportBuilder,
+ rootTypeBuilder.withImports(clientImportBuilder),
+ rootSchemaBuilder.withImports(clientImportBuilder),
+ {
+ enableRuntimeResponseValidation: config.enableRuntimeResponseValidation,
+ enableTypedBasePaths: config.enableTypedBasePaths,
+ },
+ )
+
+ input.allOperations().map((it) => fetchClientBuilder.add(it))
+
+ await emitter.emitGenerationResult([
+ ...serverRouters,
+ fetchClientBuilder.toCompilationUnit(),
+ rootTypeBuilder.toCompilationUnit(),
+ rootSchemaBuilder.toCompilationUnit(),
+ ])
+}
+
+async function loadExistingRouteImplementation({
+ fsAdaptor,
+ project,
+ destinationDirectory,
+ nextJsAppRouterPath,
+}: {
+ fsAdaptor: IFsAdaptor
+ project: Project
+ destinationDirectory: string
+ nextJsAppRouterPath: string
+}): Promise {
+ const exists = await fsAdaptor.exists(
+ path.join(destinationDirectory, nextJsAppRouterPath),
+ )
+
+ const source = exists
+ ? await fsAdaptor.readFile(
+ path.join(destinationDirectory, nextJsAppRouterPath),
+ )
+ : ""
+
+ return project.createSourceFile(nextJsAppRouterPath, source)
+}
+
+function routeToNextJSFilepath(route: string): string {
+ const parts = route
+ .split("/")
+ .map((part) => part.replaceAll("{", "[").replaceAll("}", "]"))
+
+ parts.push("route.ts")
+
+ return path.join(...parts)
+}
diff --git a/packages/typescript-nextjs-runtime/README.md b/packages/typescript-nextjs-runtime/README.md
new file mode 100644
index 000000000..483c37be5
--- /dev/null
+++ b/packages/typescript-nextjs-runtime/README.md
@@ -0,0 +1,10 @@
+# @nahkies/typescript-nextjs-runtime
+
+[](https://github.com/mnahkies/openapi-code-generator/actions?query=branch%3Amain+event%3Apush)
+[](https://www.npmjs.com/package/@nahkies/typescript-nextjs-runtime)
+
+This is a supporting package for code generated using [@nahkies/openapi-code-generator](https://www.npmjs.com/package/@nahkies/openapi-code-generator) using the Typescript NextJS server stubs template.
+
+You can [read the docs](https://openapi-code-generator.nahkies.co.nz/guides/server-templates/typescript-nextjs) to find out more!
+
+It's not intended by be used standalone. Similar in spirit to [tslib](https://www.npmjs.com/package/tslib)
diff --git a/packages/typescript-nextjs-runtime/package.json b/packages/typescript-nextjs-runtime/package.json
new file mode 100644
index 000000000..acf7db7c9
--- /dev/null
+++ b/packages/typescript-nextjs-runtime/package.json
@@ -0,0 +1,117 @@
+{
+ "name": "@nahkies/typescript-nextjs-runtime",
+ "version": "0.20.1",
+ "description": "Runtime package for code generated by @nahkies/openapi-code-generator using the typescript-nextjs template",
+ "license": "MIT",
+ "author": {
+ "name": "Michael Nahkies",
+ "email": "support@nahkies.co.nz"
+ },
+ "homepage": "https://openapi-code-generator.nahkies.co.nz/",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/mnahkies/openapi-code-generator.git",
+ "directory": "packages/typescript-nextjs-runtime"
+ },
+ "bugs": {
+ "url": "https://github.com/mnahkies/openapi-code-generator/issues"
+ },
+ "exports": {
+ "./errors": {
+ "import": {
+ "types": "./dist/esm/errors.d.mts",
+ "default": "./dist/esm/errors.mjs"
+ },
+ "require": {
+ "types": "./dist/cjs/errors.d.cts",
+ "default": "./dist/cjs/errors.cjs"
+ }
+ },
+ "./server": {
+ "import": {
+ "types": "./dist/esm/server.d.mts",
+ "default": "./dist/esm/server.mjs"
+ },
+ "require": {
+ "types": "./dist/cjs/server.d.cts",
+ "default": "./dist/cjs/server.cjs"
+ }
+ },
+ "./joi": {
+ "import": {
+ "types": "./dist/esm/joi.d.mts",
+ "default": "./dist/esm/joi.mjs"
+ },
+ "require": {
+ "types": "./dist/cjs/joi.d.cts",
+ "default": "./dist/cjs/joi.cjs"
+ }
+ },
+ "./zod-v3": {
+ "import": {
+ "types": "./dist/esm/zod-v3.d.mts",
+ "default": "./dist/esm/zod-v3.mjs"
+ },
+ "require": {
+ "types": "./dist/cjs/zod-v3.d.cts",
+ "default": "./dist/cjs/zod-v3.cjs"
+ }
+ },
+ "./zod-v4": {
+ "import": {
+ "types": "./dist/esm/zod-v4.d.mts",
+ "default": "./dist/esm/zod-v4.mjs"
+ },
+ "require": {
+ "types": "./dist/cjs/zod-v4.d.cts",
+ "default": "./dist/cjs/zod-v4.cjs"
+ }
+ }
+ },
+ "scripts": {
+ "clean": "rm -rf ./dist && rm -f tsconfig.tsbuildinfo",
+ "build": "tsc -p tsconfig.json",
+ "bundle": "tsdown",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "@nahkies/typescript-common-runtime": "workspace:^"
+ },
+ "peerDependencies": {
+ "joi": "^17.13.3 || ^18.0.1",
+ "next": "catalog:",
+ "zod": "^4.3.6"
+ },
+ "peerDependenciesMeta": {
+ "joi": {
+ "optional": true
+ },
+ "zod": {
+ "optional": true
+ }
+ },
+ "devDependencies": {
+ "joi": "catalog:",
+ "tsdown": "catalog:",
+ "typescript": "catalog:typescript7",
+ "vitest": "catalog:",
+ "zod": "^4.4.3"
+ },
+ "files": [
+ "src",
+ "dist",
+ "README.md",
+ "CHANGELOG.md",
+ "tsconfig.json"
+ ],
+ "keywords": [
+ "@nahkies/openapi-code-generator",
+ "runtime",
+ "typescript-nextjs",
+ "nextjs",
+ "zod"
+ ],
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/typescript-nextjs-runtime/src/errors.ts b/packages/typescript-nextjs-runtime/src/errors.ts
new file mode 100644
index 000000000..4410ef1d5
--- /dev/null
+++ b/packages/typescript-nextjs-runtime/src/errors.ts
@@ -0,0 +1,50 @@
+export enum RequestInputType {
+ RouteParam = "route params",
+ QueryString = "querystring",
+ RequestBody = "request body",
+ RequestHeader = "request header",
+}
+
+export class OpenAPIRuntimeError extends Error {
+ private constructor(
+ message: string,
+ cause: unknown,
+ public readonly phase:
+ | "request_validation"
+ | "request_handler"
+ | "response_validation",
+ ) {
+ super(message, {cause})
+ }
+
+ static RequestError(
+ cause: unknown,
+ inputType: RequestInputType,
+ ): OpenAPIRuntimeError {
+ return new OpenAPIRuntimeError(
+ `Request validation failed parsing ${inputType}`,
+ cause,
+ "request_validation",
+ )
+ }
+
+ static HandlerError(cause: unknown) {
+ return new OpenAPIRuntimeError(
+ "Request handler threw unhandled exception",
+ cause,
+ "request_handler",
+ )
+ }
+
+ static ResponseError(cause: unknown) {
+ return new OpenAPIRuntimeError(
+ "Response body failed validation",
+ cause,
+ "response_validation",
+ )
+ }
+
+ static isOpenAPIError(err: unknown): err is OpenAPIRuntimeError {
+ return err instanceof OpenAPIRuntimeError
+ }
+}
diff --git a/packages/typescript-nextjs-runtime/src/joi.ts b/packages/typescript-nextjs-runtime/src/joi.ts
new file mode 100644
index 000000000..d5573006a
--- /dev/null
+++ b/packages/typescript-nextjs-runtime/src/joi.ts
@@ -0,0 +1,80 @@
+import type {Schema as JoiSchema} from "joi"
+import {OpenAPIRuntimeError, type RequestInputType} from "./errors"
+
+// Note: joi types don't appear to have an equivalent of z.infer,
+// hence any seems about as good as we can do here.
+export function parseRequestInput(
+ schema: Schema,
+ input: unknown,
+ type: RequestInputType,
+ // biome-ignore lint/suspicious/noExplicitAny: needed
+): any
+export function parseRequestInput(
+ schema: undefined,
+ input: unknown,
+ type: RequestInputType,
+): undefined
+export function parseRequestInput(
+ schema: Schema | undefined,
+ input: unknown,
+ type: RequestInputType,
+ // biome-ignore lint/suspicious/noExplicitAny: needed
+): any {
+ try {
+ if (!schema) {
+ return undefined
+ }
+
+ const result = schema.validate(input, {stripUnknown: true})
+
+ if (result.error) {
+ throw result.error
+ }
+
+ return result.value
+ } catch (err) {
+ throw OpenAPIRuntimeError.RequestError(err, type)
+ }
+}
+
+export function responseValidationFactory(
+ possibleResponses: [string, JoiSchema][],
+ defaultResponse?: JoiSchema,
+) {
+ // Exploit the natural ordering matching the desired specificity of eg: 404 vs 4xx
+ possibleResponses.sort((x, y) => (x[0] < y[0] ? -1 : 1))
+
+ return (status: number, value: unknown) => {
+ try {
+ for (const [match, schema] of possibleResponses) {
+ const isMatch =
+ (/^\d+$/.test(match) && String(status) === match) ||
+ (/^\d[xX]{2}$/.test(match) && String(status)[0] === match[0])
+
+ if (isMatch) {
+ const result = schema.validate(value)
+
+ if (result.error) {
+ throw result.error
+ }
+
+ return result.value
+ }
+ }
+
+ if (defaultResponse) {
+ const result = defaultResponse.validate(value)
+
+ if (result.error) {
+ throw result.error
+ }
+
+ return result.value
+ }
+
+ return value
+ } catch (err) {
+ throw OpenAPIRuntimeError.ResponseError(err)
+ }
+ }
+}
diff --git a/packages/typescript-nextjs-runtime/src/server.ts b/packages/typescript-nextjs-runtime/src/server.ts
new file mode 100644
index 000000000..a25f6987b
--- /dev/null
+++ b/packages/typescript-nextjs-runtime/src/server.ts
@@ -0,0 +1,41 @@
+import type {
+ Res,
+ // SkipResponse,
+ StatusCode,
+} from "@nahkies/typescript-common-runtime/types"
+
+export {parseQueryParameters} from "@nahkies/typescript-common-runtime/query-parser"
+export {
+ type Params,
+ type Res,
+ SkipResponse,
+ type StatusCode,
+ type StatusCode1xx,
+ type StatusCode2xx,
+ type StatusCode3xx,
+ type StatusCode4xx,
+ type StatusCode5xx,
+} from "@nahkies/typescript-common-runtime/types"
+
+export class OpenAPIRuntimeResponse {
+ private _body?: Type
+
+ constructor(private readonly status: StatusCode) {}
+
+ body(body: Type): this {
+ this._body = body
+ return this
+ }
+
+ unpack(): Res {
+ return {status: this.status, body: this._body}
+ }
+}
+
+export type OpenAPIRuntimeResponder<
+ Status extends StatusCode = StatusCode,
+ // biome-ignore lint/suspicious/noExplicitAny: needed
+ Type = any,
+> = {
+ withStatus: (status: Status) => OpenAPIRuntimeResponse
+}
diff --git a/packages/typescript-nextjs-runtime/src/zod-v3.ts b/packages/typescript-nextjs-runtime/src/zod-v3.ts
new file mode 100644
index 000000000..3664aaf9d
--- /dev/null
+++ b/packages/typescript-nextjs-runtime/src/zod-v3.ts
@@ -0,0 +1,56 @@
+import type {z} from "zod/v3"
+import {OpenAPIRuntimeError, type RequestInputType} from "./errors"
+
+export function parseRequestInput(
+ schema: Schema,
+ input: unknown,
+ type: RequestInputType,
+): z.infer
+export function parseRequestInput(
+ schema: undefined,
+ input: unknown,
+ type: RequestInputType,
+): undefined
+export function parseRequestInput(
+ schema: Schema | undefined,
+ input: unknown,
+ type: RequestInputType,
+): z.infer | undefined {
+ try {
+ return schema?.parse(input)
+ } catch (err) {
+ throw OpenAPIRuntimeError.RequestError(err, type)
+ }
+}
+
+// TODO: optional response validation
+export function responseValidationFactory(
+ possibleResponses: [string, z.ZodTypeAny][],
+ defaultResponse?: z.ZodTypeAny,
+) {
+ // Exploit the natural ordering matching the desired specificity of eg: 404 vs 4xx
+ possibleResponses.sort((x, y) => (x[0] < y[0] ? -1 : 1))
+
+ return (status: number, value: unknown) => {
+ try {
+ for (const [match, schema] of possibleResponses) {
+ const isMatch =
+ (/^\d+$/.test(match) && String(status) === match) ||
+ (/^\d[xX]{2}$/.test(match) && String(status)[0] === match[0])
+
+ if (isMatch) {
+ return schema.parse(value)
+ }
+ }
+
+ if (defaultResponse) {
+ return defaultResponse.parse(value)
+ }
+
+ // TODO: throw on unmatched response
+ return value
+ } catch (err) {
+ throw OpenAPIRuntimeError.ResponseError(err)
+ }
+ }
+}
diff --git a/packages/typescript-nextjs-runtime/src/zod-v4.ts b/packages/typescript-nextjs-runtime/src/zod-v4.ts
new file mode 100644
index 000000000..575816ef7
--- /dev/null
+++ b/packages/typescript-nextjs-runtime/src/zod-v4.ts
@@ -0,0 +1,56 @@
+import type {z} from "zod/v4"
+import {OpenAPIRuntimeError, type RequestInputType} from "./errors"
+
+export function parseRequestInput(
+ schema: Schema,
+ input: unknown,
+ type: RequestInputType,
+): z.infer
+export function parseRequestInput(
+ schema: undefined,
+ input: unknown,
+ type: RequestInputType,
+): undefined
+export function parseRequestInput(
+ schema: Schema | undefined,
+ input: unknown,
+ type: RequestInputType,
+): z.infer | undefined {
+ try {
+ return schema?.parse(input)
+ } catch (err) {
+ throw OpenAPIRuntimeError.RequestError(err, type)
+ }
+}
+
+// TODO: optional response validation
+export function responseValidationFactory(
+ possibleResponses: [string, z.ZodTypeAny][],
+ defaultResponse?: z.ZodTypeAny,
+) {
+ // Exploit the natural ordering matching the desired specificity of eg: 404 vs 4xx
+ possibleResponses.sort((x, y) => (x[0] < y[0] ? -1 : 1))
+
+ return (status: number, value: unknown) => {
+ try {
+ for (const [match, schema] of possibleResponses) {
+ const isMatch =
+ (/^\d+$/.test(match) && String(status) === match) ||
+ (/^\d[xX]{2}$/.test(match) && String(status)[0] === match[0])
+
+ if (isMatch) {
+ return schema.parse(value)
+ }
+ }
+
+ if (defaultResponse) {
+ return defaultResponse.parse(value)
+ }
+
+ // TODO: throw on unmatched response
+ return value
+ } catch (err) {
+ throw OpenAPIRuntimeError.ResponseError(err)
+ }
+ }
+}
diff --git a/packages/typescript-nextjs-runtime/tsconfig.json b/packages/typescript-nextjs-runtime/tsconfig.json
new file mode 100644
index 000000000..368510a53
--- /dev/null
+++ b/packages/typescript-nextjs-runtime/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "references": [
+ {
+ "path": "../typescript-common-runtime"
+ }
+ ]
+}
diff --git a/packages/typescript-nextjs-runtime/tsdown.config.mts b/packages/typescript-nextjs-runtime/tsdown.config.mts
new file mode 100644
index 000000000..aee5751c3
--- /dev/null
+++ b/packages/typescript-nextjs-runtime/tsdown.config.mts
@@ -0,0 +1,34 @@
+import {defineConfig} from "tsdown"
+import packageJson from "./package.json" with {type: "json"}
+
+export default defineConfig({
+ name: packageJson.name,
+ entry: [
+ "./src/server.ts",
+ "./src/errors.ts",
+ "./src/joi.ts",
+ "./src/zod-v4.ts",
+ "./src/zod-v3.ts",
+ ],
+
+ target: "esnext",
+ dts: true,
+ sourcemap: true,
+ failOnWarn: true,
+ logLevel: "warn",
+ publint: true,
+ attw: {profile: "node16"},
+
+ deps: {
+ onlyBundle: ["@nahkies/typescript-common-runtime"],
+ },
+
+ format: {
+ esm: {
+ outDir: "./dist/esm",
+ },
+ cjs: {
+ outDir: "./dist/cjs",
+ },
+ },
+})
diff --git a/packages/typescript-nextjs-runtime/vitest.config.mts b/packages/typescript-nextjs-runtime/vitest.config.mts
new file mode 100644
index 000000000..2420afb2d
--- /dev/null
+++ b/packages/typescript-nextjs-runtime/vitest.config.mts
@@ -0,0 +1,10 @@
+import {defineConfig} from "vitest/config"
+import {baseTestConfig} from "../../vitest.base"
+import pkg from "./package.json" with {type: "json"}
+
+export default defineConfig({
+ test: {
+ ...baseTestConfig.test,
+ name: pkg.name,
+ },
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2fec1fc3c..c01519fbd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -64,8 +64,8 @@ catalogs:
specifier: ^1.17.6
version: 1.17.6
'@koa/router':
- specifier: ^15.6.0
- version: 15.6.0
+ specifier: ^15.7.0
+ version: 15.7.0
'@lerna-lite/cli':
specifier: ^5.4.0
version: 5.4.0
@@ -217,7 +217,7 @@ importers:
dependencies:
'@koa/router':
specifier: 'catalog:'
- version: 15.6.0(koa@3.2.1)
+ version: 15.7.0(koa@3.2.1)
'@nahkies/typescript-axios-runtime':
specifier: workspace:*
version: link:../packages/typescript-axios-runtime
@@ -283,10 +283,10 @@ importers:
specifier: 'catalog:'
version: 22.0.5(@angular/common@22.0.5(@angular/core@22.0.5(@angular/compiler@22.0.5)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.0.5(@angular/compiler@22.0.5)(rxjs@7.8.2))(@angular/platform-browser@22.0.5(@angular/common@22.0.5(@angular/core@22.0.5(@angular/compiler@22.0.5)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.0.5(@angular/compiler@22.0.5)(rxjs@7.8.2)))(rxjs@7.8.2)
rxjs:
- specifier: ~7.8.0
+ specifier: ~7.8.2
version: 7.8.2
tslib:
- specifier: ^2.3.0
+ specifier: ^2.8.1
version: 2.8.1
devDependencies:
'@angular/build':
@@ -378,7 +378,7 @@ importers:
dependencies:
'@koa/router':
specifier: 'catalog:'
- version: 15.6.0(koa@3.2.1)
+ version: 15.7.0(koa@3.2.1)
'@nahkies/typescript-koa-runtime':
specifier: workspace:*
version: link:../../packages/typescript-koa-runtime
@@ -402,6 +402,43 @@ importers:
specifier: catalog:typescript7
version: 7.0.2
+ integration-tests/typescript-nextjs:
+ dependencies:
+ '@nahkies/typescript-fetch-runtime':
+ specifier: workspace:*
+ version: link:../../packages/typescript-fetch-runtime
+ '@nahkies/typescript-nextjs-runtime':
+ specifier: workspace:*
+ version: link:../../packages/typescript-nextjs-runtime
+ joi:
+ specifier: 'catalog:'
+ version: 18.2.3
+ next:
+ specifier: 'catalog:'
+ version: 16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.99.0)
+ react:
+ specifier: 'catalog:'
+ version: 19.2.7
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.7(react@19.2.7)
+ zod:
+ specifier: ^4.4.3
+ version: 4.4.3
+ devDependencies:
+ '@types/node':
+ specifier: ^22.20.0
+ version: 22.20.0
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.2.17
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.17)
+ typescript:
+ specifier: catalog:typescript6
+ version: 6.0.3
+
packages/documentation:
dependencies:
'@hookform/resolvers':
@@ -504,6 +541,9 @@ importers:
prettier:
specifier: 'catalog:'
version: 3.9.4
+ ts-morph:
+ specifier: ^26.0.0
+ version: 26.0.0
tslib:
specifier: ^2.8.1
version: 2.8.1
@@ -536,19 +576,19 @@ importers:
specifier: ^4.17.24
version: 4.17.24
'@typespec/compiler':
- specifier: ^1.12.0
+ specifier: ^1.13.0
version: 1.13.0(@types/node@22.20.0)
'@typespec/events':
specifier: 0.83.0
version: 0.83.0(@typespec/compiler@1.13.0(@types/node@22.20.0))
'@typespec/http':
- specifier: ^1.12.0
+ specifier: ^1.13.0
version: 1.13.0(@typespec/compiler@1.13.0(@types/node@22.20.0))(@typespec/streams@0.83.0(@typespec/compiler@1.13.0(@types/node@22.20.0)))
'@typespec/openapi':
- specifier: ^1.12.0
+ specifier: ^1.13.0
version: 1.13.0(@typespec/compiler@1.13.0(@types/node@22.20.0))(@typespec/http@1.13.0(@typespec/compiler@1.13.0(@types/node@22.20.0))(@typespec/streams@0.83.0(@typespec/compiler@1.13.0(@types/node@22.20.0))))
'@typespec/openapi3':
- specifier: ^1.12.0
+ specifier: ^1.13.0
version: 1.13.0(d40defdf02a20bbce34f949d2f323956)
'@typespec/rest':
specifier: 0.83.0
@@ -704,7 +744,7 @@ importers:
version: 5.0.0
'@koa/router':
specifier: 'catalog:'
- version: 15.6.0(koa@3.2.1)
+ version: 15.7.0(koa@3.2.1)
'@nahkies/typescript-common-runtime':
specifier: workspace:^
version: link:../typescript-common-runtime
@@ -736,6 +776,31 @@ importers:
specifier: ^4.4.3
version: 4.4.3
+ packages/typescript-nextjs-runtime:
+ dependencies:
+ '@nahkies/typescript-common-runtime':
+ specifier: workspace:^
+ version: link:../typescript-common-runtime
+ next:
+ specifier: 'catalog:'
+ version: 16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.99.0)
+ devDependencies:
+ joi:
+ specifier: 'catalog:'
+ version: 18.2.3
+ tsdown:
+ specifier: 'catalog:'
+ version: 0.22.5(@arethetypeswrong/core@0.18.4)(publint@0.3.21)(tsx@4.23.0)(typescript@7.0.2)
+ typescript:
+ specifier: catalog:typescript7
+ version: 7.0.2
+ vitest:
+ specifier: 'catalog:'
+ version: 4.1.9(@types/node@22.20.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@1.8.0))(vite@7.3.5(@types/node@22.20.0)(sass@1.99.0)(tsx@4.23.0)(yaml@2.9.0))
+ zod:
+ specifier: ^4.4.3
+ version: 4.4.3
+
packages:
'@algolia/abtesting@1.18.0':
@@ -1674,8 +1739,8 @@ packages:
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
- '@iconify/utils@3.1.3':
- resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==}
+ '@iconify/utils@3.1.4':
+ resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==}
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
@@ -2042,8 +2107,8 @@ packages:
resolution: {integrity: sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==}
engines: {node: '>= 14.0.0'}
- '@koa/router@15.6.0':
- resolution: {integrity: sha512-iEOXlvGIBqSNkGXrg0XtMARAOm5zA24oedXxiTGEkrD4JgwVjfRDddCQvW1s4WEcwDYvyecRbf8BikXsuEEj8w==}
+ '@koa/router@15.7.0':
+ resolution: {integrity: sha512-WaAlk4TOl/O0rhTpOR0l052gz03syPMmI6Pe2gd7v3ubjfv5UcSGcnb0Y/J5NNC/ln+5FiUqPJTc/a15I+XqAA==}
engines: {node: '>= 20'}
peerDependencies:
koa: ^2.0.0 || ^3.0.0
@@ -2141,8 +2206,8 @@ packages:
'@mdx-js/mdx@3.1.1':
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
- '@mermaid-js/parser@1.1.1':
- resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==}
+ '@mermaid-js/parser@1.2.0':
+ resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==}
'@modelcontextprotocol/sdk@1.29.0':
resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==}
@@ -3089,16 +3154,16 @@ packages:
cpu: [x64]
os: [win32]
- '@scalar/helpers@0.8.2':
- resolution: {integrity: sha512-qNbqUjSB3S4Gr4A0oANcm5G1Ip+EqBxICYKhe9YzmnaBpbmW6shxqpiivApTvvuDf+uIhR3uMwWyVQbYcGLsxA==}
+ '@scalar/helpers@0.9.0':
+ resolution: {integrity: sha512-M34CLRCttqC1bXthI/QSzQj0s5C6nrU2PFWf/vOT3RpycbiGDGQbqR+5RfFzpOIQvRqbHfNdcRbeiZBw+vCbkQ==}
engines: {node: '>=22'}
- '@scalar/json-magic@0.12.16':
- resolution: {integrity: sha512-w8cDbZhHCzmIblWx92IVWoAXsbI4Fz3m++jiBANTSO1hgphF6UqEPQiCt3wnMPaxaanjMQxjS/iBk1UGXR2EGA==}
+ '@scalar/json-magic@0.12.17':
+ resolution: {integrity: sha512-Vw2nrUDIjhvMP6vxFtkiiFlabJ6SyTtfn1BsOxgnr1hIB+/rkngMguiDzl5em21VjyfFGIoADia+QWKM2hdcdA==}
engines: {node: '>=22'}
- '@scalar/openapi-parser@0.28.7':
- resolution: {integrity: sha512-E6beEdTsJxUStxOmY1knQvSNJq6LTiXOsRX2WTrfmU6d/kiATn6IKkAU0kXtAZkaYCGU4UCEmBFHCMmNKn0JLA==}
+ '@scalar/openapi-parser@0.28.8':
+ resolution: {integrity: sha512-BO98D3TLfKNL80UnE1sIAmI6DQRmOaH0AHnlAooTn1sQjNbRDaeHyRO53EEjo7HjFEdHeQtiRzoXmMB4jvt8AA==}
engines: {node: '>=22'}
'@scalar/openapi-types@0.9.1':
@@ -3194,14 +3259,14 @@ packages:
'@swc/helpers@0.5.23':
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
- '@tanstack/react-virtual@3.14.3':
- resolution: {integrity: sha512-k/cnHPVaOfn46hSbiY6n4Dzf4QjCGWSF40zR5QIIYUqPAjpA6TN7InfYmcMiDVQGP2iUn9xsRbAl8u1v3UmeVQ==}
+ '@tanstack/react-virtual@3.14.5':
+ resolution: {integrity: sha512-4EKRXh7zBLkbKbFmG3AUVkircuHd+7OdT1pocJSepxtfBd3qnrJgJ5rtPkRYyo9fmyVb2+pI2xPy5oYvMLQy6A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- '@tanstack/virtual-core@3.17.1':
- resolution: {integrity: sha512-VZyW2Uiml5tmBZwPGrSD3Sz73OxzljQMCmzYHsUTPEuTsERf5xwa+uWb01xEzkz3ZSYTjj8NEb/mKHvgKxyZdA==}
+ '@tanstack/virtual-core@3.17.3':
+ resolution: {integrity: sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw==}
'@theguild/remark-mermaid@0.3.0':
resolution: {integrity: sha512-Fy1J4FSj8totuHsHFpaeWyWRaRSIvpzGTRoEfnNJc1JmLV9uV70sYE3zcT+Jj5Yw20Xq4iCsiT+3Ho49BBZcBQ==}
@@ -3211,6 +3276,9 @@ packages:
'@theguild/remark-npm2yarn@0.3.3':
resolution: {integrity: sha512-ma6DvR03gdbvwqfKx1omqhg9May/VYGdMHvTzB4VuxkyS7KzfZ/lzrj43hmcsggpMje0x7SADA/pcMph0ejRnA==}
+ '@ts-morph/common@0.27.0':
+ resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==}
+
'@ts-morph/common@0.28.1':
resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==}
@@ -3315,8 +3383,8 @@ packages:
'@types/d3-quadtree@3.0.6':
resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
- '@types/d3-random@3.0.3':
- resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
+ '@types/d3-random@3.0.4':
+ resolution: {integrity: sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==}
'@types/d3-scale-chromatic@3.1.0':
resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
@@ -4005,8 +4073,8 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
- baseline-browser-mapping@2.10.41:
- resolution: {integrity: sha512-WwS7MHhqGHHlaVsqRZnhvCEMS0owDX+SxRlve7JkuH7My1Ara3ZriTmCQupPfYjxMZ8I/tgxtJYr2t7taHaH4A==}
+ baseline-browser-mapping@2.10.42:
+ resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -4646,8 +4714,8 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
- es-toolkit@1.47.1:
- resolution: {integrity: sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==}
+ es-toolkit@1.49.0:
+ resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==}
esast-util-from-estree@2.0.0:
resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
@@ -4852,8 +4920,8 @@ packages:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
- fs-extra@11.3.5:
- resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==}
+ fs-extra@11.3.6:
+ resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==}
engines: {node: '>=14.14'}
fs-minipass@3.0.3:
@@ -5544,8 +5612,8 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
- mermaid@11.15.0:
- resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==}
+ mermaid@11.16.0:
+ resolution: {integrity: sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==}
mhchemparser@4.2.1:
resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==}
@@ -6126,8 +6194,8 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
- qs@6.15.2:
- resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==}
+ qs@6.15.3:
+ resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==}
engines: {node: '>=0.6'}
quansync@1.0.0:
@@ -6136,8 +6204,8 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
- range-parser@1.2.1:
- resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+ range-parser@1.3.0:
+ resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==}
engines: {node: '>= 0.6'}
raw-body@2.5.3:
@@ -6615,8 +6683,8 @@ packages:
resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==}
engines: {node: '>=18'}
- tabbable@6.4.0:
- resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
+ tabbable@6.5.0:
+ resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==}
tagged-tag@1.0.0:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
@@ -6700,6 +6768,9 @@ packages:
resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==}
engines: {node: '>=6.10'}
+ ts-morph@26.0.0:
+ resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==}
+
ts-morph@27.0.2:
resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==}
@@ -6753,16 +6824,16 @@ packages:
resolution: {integrity: sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==}
engines: {node: ^20.17.0 || >=22.9.0}
- twoslash-protocol@0.3.8:
- resolution: {integrity: sha512-HmvAHoiEviK8LqvAQyc9/irkdvwTUiR1fHmNwH/0gq8EHxyBt4PWVPixjEXg6wJu1u6yBrILEWXGK9Kw58/8yQ==}
+ twoslash-protocol@0.3.9:
+ resolution: {integrity: sha512-9/iwp+CXOnjFMPQuPL5PkuRbZnDoNpBvtJCLs9t8kDYkL3YHujbvnHfZA1i5fApDftVEdBw+T/4F+dH5kIzpYQ==}
- twoslash@0.3.8:
- resolution: {integrity: sha512-OeDz0kDl8sqPUN3nr7gqcvOs70f5lZsdhKYTX3/SgB9OvdadzzoYJI/4SBXhXV1HG8E9fLc+e17itoRYTxmoig==}
+ twoslash@0.3.9:
+ resolution: {integrity: sha512-rDclk+OtzuTX+tnea7DYLCkqGQ3eP0IyfD+kzUJ7t46X/NzlaxwrhecmEBNuSCuEn3V+n1PhcjUUQQ7gUJzX5Q==}
peerDependencies:
typescript: ^5.5.0 || ^6.0.0
- type-fest@5.7.0:
- resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==}
+ type-fest@5.8.0:
+ resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==}
engines: {node: '>=20'}
type-is@1.6.18:
@@ -7911,7 +7982,7 @@ snapshots:
'@floating-ui/utils': 0.2.11
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
- tabbable: 6.4.0
+ tabbable: 6.5.0
'@floating-ui/utils@0.2.11': {}
@@ -7949,7 +8020,7 @@ snapshots:
'@floating-ui/react': 0.26.28(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@react-aria/focus': 3.22.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@react-aria/interactions': 3.28.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
- '@tanstack/react-virtual': 3.14.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@tanstack/react-virtual': 3.14.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
use-sync-external-store: 1.6.0(react@19.2.7)
@@ -8006,7 +8077,7 @@ snapshots:
'@iconify/types@2.0.0': {}
- '@iconify/utils@3.1.3':
+ '@iconify/utils@3.1.4':
dependencies:
'@antfu/install-pkg': 1.1.0
'@iconify/types': 2.0.0
@@ -8318,7 +8389,7 @@ snapshots:
dependencies:
vary: 1.1.2
- '@koa/router@15.6.0(koa@3.2.1)':
+ '@koa/router@15.7.0(koa@3.2.1)':
dependencies:
debug: 4.4.3
http-errors: 2.0.1
@@ -8493,7 +8564,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@mermaid-js/parser@1.1.1':
+ '@mermaid-js/parser@1.2.0':
dependencies:
'@chevrotain/types': 11.1.2
@@ -9208,18 +9279,18 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.62.2':
optional: true
- '@scalar/helpers@0.8.2': {}
+ '@scalar/helpers@0.9.0': {}
- '@scalar/json-magic@0.12.16':
+ '@scalar/json-magic@0.12.17':
dependencies:
- '@scalar/helpers': 0.8.2
+ '@scalar/helpers': 0.9.0
pathe: 2.0.3
yaml: 2.9.0
- '@scalar/openapi-parser@0.28.7':
+ '@scalar/openapi-parser@0.28.8':
dependencies:
- '@scalar/helpers': 0.8.2
- '@scalar/json-magic': 0.12.16
+ '@scalar/helpers': 0.9.0
+ '@scalar/json-magic': 0.12.17
'@scalar/openapi-types': 0.9.1
'@scalar/openapi-upgrader': 0.2.9
ajv: 8.20.0
@@ -9274,7 +9345,7 @@ snapshots:
dependencies:
'@shikijs/core': 3.23.0
'@shikijs/types': 3.23.0
- twoslash: 0.3.8(typescript@6.0.3)
+ twoslash: 0.3.9(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@@ -9345,17 +9416,17 @@ snapshots:
dependencies:
tslib: 2.8.1
- '@tanstack/react-virtual@3.14.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ '@tanstack/react-virtual@3.14.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
- '@tanstack/virtual-core': 3.17.1
+ '@tanstack/virtual-core': 3.17.3
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
- '@tanstack/virtual-core@3.17.1': {}
+ '@tanstack/virtual-core@3.17.3': {}
'@theguild/remark-mermaid@0.3.0(react@19.2.7)':
dependencies:
- mermaid: 11.15.0
+ mermaid: 11.16.0
react: 19.2.7
unist-util-visit: 5.1.0
@@ -9364,6 +9435,12 @@ snapshots:
npm-to-yarn: 3.0.1
unist-util-visit: 5.1.0
+ '@ts-morph/common@0.27.0':
+ dependencies:
+ fast-glob: 3.3.3
+ minimatch: 10.2.5
+ path-browserify: 1.0.1
+
'@ts-morph/common@0.28.1':
dependencies:
minimatch: 10.2.5
@@ -9477,7 +9554,7 @@ snapshots:
'@types/d3-quadtree@3.0.6': {}
- '@types/d3-random@3.0.3': {}
+ '@types/d3-random@3.0.4': {}
'@types/d3-scale-chromatic@3.1.0': {}
@@ -9528,7 +9605,7 @@ snapshots:
'@types/d3-path': 3.1.1
'@types/d3-polygon': 3.0.2
'@types/d3-quadtree': 3.0.6
- '@types/d3-random': 3.0.3
+ '@types/d3-random': 3.0.4
'@types/d3-scale': 4.0.9
'@types/d3-scale-chromatic': 3.1.0
'@types/d3-selection': 3.0.11
@@ -9779,8 +9856,8 @@ snapshots:
'@typespec/openapi3@1.13.0(d40defdf02a20bbce34f949d2f323956)':
dependencies:
- '@scalar/json-magic': 0.12.16
- '@scalar/openapi-parser': 0.28.7
+ '@scalar/json-magic': 0.12.17
+ '@scalar/openapi-parser': 0.28.8
'@scalar/openapi-types': 0.9.1
'@typespec/asset-emitter': 0.79.1(@typespec/compiler@1.13.0(@types/node@22.20.0))
'@typespec/compiler': 1.13.0(@types/node@22.20.0)
@@ -10079,7 +10156,7 @@ snapshots:
balanced-match@4.0.4: {}
- baseline-browser-mapping@2.10.41: {}
+ baseline-browser-mapping@2.10.42: {}
beasties@0.4.2:
dependencies:
@@ -10120,7 +10197,7 @@ snapshots:
http-errors: 2.0.1
iconv-lite: 0.7.3
on-finished: 2.4.1
- qs: 6.15.2
+ qs: 6.15.3
raw-body: 3.0.2
type-is: 2.1.0
transitivePeerDependencies:
@@ -10138,7 +10215,7 @@ snapshots:
browserslist@4.28.4:
dependencies:
- baseline-browser-mapping: 2.10.41
+ baseline-browser-mapping: 2.10.42
caniuse-lite: 1.0.30001800
electron-to-chromium: 1.5.387
node-releases: 2.0.50
@@ -10247,7 +10324,7 @@ snapshots:
dependencies:
'@hapi/bourne': 3.0.0
inflation: 2.1.0
- qs: 6.15.2
+ qs: 6.15.3
raw-body: 2.5.3
type-is: 1.6.18
@@ -10708,7 +10785,7 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.4
- es-toolkit@1.47.1: {}
+ es-toolkit@1.49.0: {}
esast-util-from-estree@2.0.0:
dependencies:
@@ -10893,8 +10970,8 @@ snapshots:
once: 1.4.0
parseurl: 1.3.3
proxy-addr: 2.0.7
- qs: 6.15.2
- range-parser: 1.2.1
+ qs: 6.15.3
+ range-parser: 1.3.0
router: 2.2.0
send: 1.2.1
serve-static: 2.2.1
@@ -10940,6 +11017,10 @@ snapshots:
dependencies:
walk-up-path: 4.0.0
+ fdir@6.5.0(picomatch@4.0.4):
+ optionalDependencies:
+ picomatch: 4.0.4
+
fdir@6.5.0(picomatch@4.0.5):
optionalDependencies:
picomatch: 4.0.5
@@ -11004,7 +11085,7 @@ snapshots:
fresh@2.0.0: {}
- fs-extra@11.3.5:
+ fs-extra@11.3.6:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.2.1
@@ -11056,7 +11137,7 @@ snapshots:
email-addresses: 5.0.0
filenamify: 4.3.0
find-cache-dir: 3.3.2
- fs-extra: 11.3.5
+ fs-extra: 11.3.6
globby: 11.1.0
git-up@8.1.1:
@@ -11611,7 +11692,7 @@ snapshots:
'@types/koa': 3.0.3
co-body: 6.2.0
formidable: 3.5.4
- type-fest: 5.7.0
+ type-fest: 5.8.0
zod: 4.4.3
koa-compose@4.1.0: {}
@@ -11982,11 +12063,11 @@ snapshots:
merge2@1.4.1: {}
- mermaid@11.15.0:
+ mermaid@11.16.0:
dependencies:
'@braintree/sanitize-url': 7.1.2
- '@iconify/utils': 3.1.3
- '@mermaid-js/parser': 1.1.1
+ '@iconify/utils': 3.1.4
+ '@mermaid-js/parser': 1.2.0
'@types/d3': 7.4.3
'@upsetjs/venn.js': 2.0.0
cytoscape: 3.34.0
@@ -11997,7 +12078,7 @@ snapshots:
dagre-d3-es: 7.0.14
dayjs: 1.11.21
dompurify: 3.4.11
- es-toolkit: 1.47.1
+ es-toolkit: 1.49.0
katex: 0.16.47
khroma: 2.1.0
marked: 16.4.2
@@ -12405,7 +12486,7 @@ snapshots:
dependencies:
'@next/env': 16.2.10
'@swc/helpers': 0.5.15
- baseline-browser-mapping: 2.10.41
+ baseline-browser-mapping: 2.10.42
caniuse-lite: 1.0.30001800
postcss: 8.4.31
react: 19.2.7
@@ -12822,15 +12903,16 @@ snapshots:
punycode@2.3.1: {}
- qs@6.15.2:
+ qs@6.15.3:
dependencies:
+ es-define-property: 1.0.1
side-channel: 1.1.1
quansync@1.0.0: {}
queue-microtask@1.2.3: {}
- range-parser@1.2.1: {}
+ range-parser@1.3.0: {}
raw-body@2.5.3:
dependencies:
@@ -13295,7 +13377,7 @@ snapshots:
mime-types: 3.0.2
ms: 2.1.3
on-finished: 2.4.1
- range-parser: 1.2.1
+ range-parser: 1.3.0
statuses: 2.0.2
transitivePeerDependencies:
- supports-color
@@ -13535,7 +13617,7 @@ snapshots:
system-architecture@0.1.0: {}
- tabbable@6.4.0: {}
+ tabbable@6.5.0: {}
tagged-tag@1.0.0: {}
@@ -13559,8 +13641,8 @@ snapshots:
tinyglobby@0.2.16:
dependencies:
- fdir: 6.5.0(picomatch@4.0.5)
- picomatch: 4.0.5
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
tinyglobby@0.2.17:
dependencies:
@@ -13609,6 +13691,11 @@ snapshots:
ts-dedent@2.3.0: {}
+ ts-morph@26.0.0:
+ dependencies:
+ '@ts-morph/common': 0.27.0
+ code-block-writer: 13.0.3
+
ts-morph@27.0.2:
dependencies:
'@ts-morph/common': 0.28.1
@@ -13688,17 +13775,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
- twoslash-protocol@0.3.8: {}
+ twoslash-protocol@0.3.9: {}
- twoslash@0.3.8(typescript@6.0.3):
+ twoslash@0.3.9(typescript@6.0.3):
dependencies:
'@typescript/vfs': 1.6.4(typescript@6.0.3)
- twoslash-protocol: 0.3.8
+ twoslash-protocol: 0.3.9
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- type-fest@5.7.0:
+ type-fest@5.8.0:
dependencies:
tagged-tag: 1.0.0
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index fb88e2548..b831d0c6e 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -18,7 +18,7 @@ catalog:
'@biomejs/wasm-nodejs': 2.5.2
'@commander-js/extra-typings': ^15.0.0
'@hyperjump/json-schema': ^1.17.6
- '@koa/router': ^15.6.0
+ '@koa/router': ^15.7.0
'@lerna-lite/cli': ^5.4.0
'@lerna-lite/publish': ^5.4.0
'@lerna-lite/version': ^5.4.0
diff --git a/scripts/generate.mjs b/scripts/generate.mjs
index 72ee88975..c10942c10 100755
--- a/scripts/generate.mjs
+++ b/scripts/generate.mjs
@@ -65,10 +65,20 @@ async function runSingle(templatePath, input) {
const filename = path.basename(input)
const template = path.basename(templatePath)
+ if (template === "typescript-nextjs" && !input.endsWith("todo-lists.yaml")) {
+ console.warn(`skipping generation of ${templatePath} using ${template}`)
+ return
+ }
+
+ const output =
+ template === "typescript-nextjs"
+ ? `integration-tests/${template}/src`
+ : `integration-tests/${template}/src/generated/${filename}`
+
const args = [
`--input="${input}"`,
`--input-type=${inputType}`,
- `--output="integration-tests/${template}/src/generated/${filename}"`,
+ `--output="${output}"`,
`--template="${template}"`,
`--schema-builder=${schemaBuilder}`,
]
@@ -95,15 +105,19 @@ function runCmd(cmd) {
console.info(cmd)
- const child = exec(cmd, (err) => {
- if (err) {
- err.output = output
- reject(err)
- return
- }
-
- resolve(output)
- })
+ const child = exec(
+ cmd,
+ {env: {...process.env, OPENAPI_INTEGRATION_TESTS: "true"}},
+ (err) => {
+ if (err) {
+ err.output = output
+ reject(err)
+ return
+ }
+
+ resolve(output)
+ },
+ )
child.stdout.on("data", (it) => {
output.push(it.trim())
diff --git a/tsconfig.json b/tsconfig.json
index 4d2528718..d90c713b0 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,6 +7,7 @@
{ "path": "./packages/typescript-axios-runtime" },
{ "path": "./packages/typescript-express-runtime" },
{ "path": "./packages/typescript-fetch-runtime" },
- { "path": "./packages/typescript-koa-runtime" }
+ { "path": "./packages/typescript-koa-runtime" },
+ { "path": "./packages/typescript-nextjs-runtime" }
]
}