Skip to content

Commit 77aa9da

Browse files
committed
feat: wip documentation
1 parent 197b376 commit 77aa9da

File tree

7 files changed

+183
-12
lines changed

7 files changed

+183
-12
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {_GET, _POST} from "../../../generated/todo-lists.yaml/attachments/route"
22

3-
export const GET = _GET(async (respond, context) => {
3+
export const GET = _GET(async (respond, request) => {
44
// TODO: implementation
55
return respond.withStatus(501).body({message: "not implemented"} as any)
66
})
7-
export const POST = _POST(async ({body}, respond, context) => {
7+
export const POST = _POST(async ({body}, respond, request) => {
88
// TODO: implementation
99
return respond.withStatus(501).body({message: "not implemented"} as any)
1010
})

integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import {
33
_POST,
44
} from "../../../../../generated/todo-lists.yaml/list/[listId]/items/route"
55

6-
export const GET = _GET(async ({params}, respond, context) => {
6+
export const GET = _GET(async ({params}, respond, request) => {
77
// TODO: implementation
88
return respond.withStatus(501).body({message: "not implemented"} as any)
99
})
10-
export const POST = _POST(async ({params, body}, respond, context) => {
10+
export const POST = _POST(async ({params, body}, respond, request) => {
1111
// TODO: implementation
1212
return respond.withStatus(501).body({message: "not implemented"} as any)
1313
})

integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import {
44
_PUT,
55
} from "../../../../generated/todo-lists.yaml/list/[listId]/route"
66

7-
export const GET = _GET(async ({params}, respond, context) => {
7+
export const GET = _GET(async ({params}, respond, request) => {
88
// TODO: implementation
99
return respond.withStatus(501).body({message: "not implemented"} as any)
1010
})
11-
export const PUT = _PUT(async ({params, body}, respond, context) => {
11+
export const PUT = _PUT(async ({params, body}, respond, request) => {
1212
// TODO: implementation
1313
return respond.withStatus(501).body({message: "not implemented"} as any)
1414
})
15-
export const DELETE = _DELETE(async ({params}, respond, context) => {
15+
export const DELETE = _DELETE(async ({params}, respond, request) => {
1616
// TODO: implementation
1717
return respond.withStatus(501).body({message: "not implemented"} as any)
1818
})
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {_GET} from "../../../generated/todo-lists.yaml/list/route"
22

3-
export const GET = _GET(async ({query}, respond, context) => {
3+
export const GET = _GET(async ({query}, respond, request) => {
44
// TODO: implementation
55
return respond.withStatus(501).body({message: "not implemented"} as any)
66
})
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import {Tabs} from 'nextra/components'
2+
3+
# Using the `typescript-nextjs` template
4+
5+
> ⚠️ **Alpha Template** ⚠️
6+
>
7+
> This template is currently in **alpha**. APIs and features are subject to change.<br/>
8+
> It might break in unexpected ways, or **mangle** your code.
9+
>
10+
> You can see an example of it used on a real project here: <br/>
11+
> https://github.com/mnahkies/spdx-dependency-track
12+
13+
14+
The `typescript-nextjs` template outputs scaffolding code that handles the following:
15+
16+
- Generates route handlers in the Next.js App Router (app/api/.../route.ts) for every operation in your OpenAPI spec
17+
- Parses and validates request input (query, params, headers, and body) using `zod`, or `joi`
18+
- Validates response types at runtime before sending them, ensuring they conform to your OpenAPI spec
19+
- Enforces full type safety for each handler’s inputs and outputs
20+
- Additionally, emits a [typescript-fetch](../client-templates/typescript-fetch) client for making requests to the routes from your react code
21+
22+
See [integration-tests/typescript-nextjs](https://github.com/mnahkies/openapi-code-generator/tree/main/integration-tests/typescript-nextjs) for more samples.
23+
24+
### Install dependencies
25+
First install the CLI and the required runtime packages to your project:
26+
```sh npm2yarn
27+
npm i --dev @nahkies/openapi-code-generator
28+
npm i @nahkies/typescript-nextjs-runtime next zod
29+
```
30+
31+
See also [quick start](../../getting-started/quick-start) guide
32+
33+
### Run generation
34+
<Tabs items={["OpenAPI3", "Typespec"]}>
35+
36+
<Tabs.Tab>
37+
```sh npm2yarn
38+
npm run openapi-code-generator \
39+
--input ./openapi.yaml \
40+
--input-type openapi3 \
41+
--output ./src \
42+
--template typescript-nextjs \
43+
--schema-builder zod
44+
```
45+
</Tabs.Tab>
46+
<Tabs.Tab>
47+
```sh npm2yarn
48+
npm run openapi-code-generator \
49+
--input ./typespec.tsp \
50+
--input-type typespec \
51+
--output ./src \
52+
--template typescript-nextjs \
53+
--schema-builder zod
54+
```
55+
</Tabs.Tab>
56+
57+
</Tabs>
58+
59+
### Using the generated code
60+
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:
61+
```shell
62+
src
63+
├── app
64+
│ ├── api
65+
│ │ └── list
66+
│ │ ├── [listId]
67+
│ │ │ ├── items
68+
│ │ │ │ └── route.ts
69+
│ │ │ └── route.ts
70+
│ │ └── route.ts
71+
│ ├── layout.tsx
72+
│ └── page.tsx
73+
└── generated
74+
├── api
75+
│ └── list
76+
│ ├── [listId]
77+
│ │ ├── items
78+
│ │ │ └── route.ts
79+
│ │ └── route.ts
80+
│ └── route.ts
81+
├── client.ts
82+
├── models.ts
83+
└── schemas.ts
84+
````
85+
86+
`./src/app/../route.ts`
87+
- a `route.ts` file is generated per operation, following Next.js App Router conventions
88+
- exports handlers (GET, POST, etc.) for each HTTP method defined
89+
- safe to edit, your route handler implementations go here
90+
- calls into `./src/generated/...` for input/output validation logic
91+
92+
`./src/generated/../route.ts`
93+
- mirror structure of the `./src/app/.../route.ts` files
94+
- contains glue code that parses input, validates responses, and calls your implementation
95+
96+
`./src/generated/models.ts`
97+
- exports plain TypeScript types for all schemas in your OpenAPI spec
98+
99+
`./src/generated/schemas.ts`
100+
- exports runtime schema validators (`zod` / `joi` depending on configuration)
101+
102+
`./src/generated/client.ts`
103+
- exports a [typescript-fetch](../client-templates/typescript-fetch) client for calling your API from frontend code
104+
- see [use-with-react-query](../use-with-react-query) for integration with `react-query`
105+
106+
#### Implementing routes
107+
108+
Once generated usage should look something like this:
109+
110+
```typescript
111+
import {db} from "../../../../../db"
112+
import {
113+
_GET,
114+
_POST,
115+
} from "../../../../../generated/todo-lists.yaml/list/[listId]/items/route"
116+
117+
export const GET = _GET(async ({params}, respond, request) => {
118+
const items = db.getTodoItems({listId: params.listId})
119+
120+
if (items) {
121+
return respond.with200().body(items)
122+
}
123+
return respond
124+
.with404()
125+
.body({code: "not-found", message: `listId ${params.listId} not found`})
126+
})
127+
128+
export const POST = _POST(async ({params, body}, respond, request) => {
129+
await db.insertTodoItem({
130+
listId: params.listId,
131+
itemId: body.id,
132+
content: body.content,
133+
completedAt: body.completedAt,
134+
})
135+
136+
return respond.with204()
137+
})
138+
139+
```
140+
141+
#### Its safe to regenerate!
142+
The template uses [ts-morph](https://ts-morph.com/) to **non-destructively generate and update** route.ts files.
143+
144+
This means you can safely add your own logic to the scaffolded files, and future regenerations will preserve your
145+
implementation code while updating the generated boilerplate.
146+
147+
#### Error Handling
148+
149+
> 🚧 Under construction
150+
>
151+
> Errors will be thrown for req/res validation issues, but currently its impossible to catch them.
152+
> More thought is needed...
153+
154+
### Escape Hatches
155+
156+
> 🚧 Under construction
157+
>
158+
> The raw nextjs `request` object is passed to your implementation, however there is not yet
159+
> a way to skip response processing.
160+
161+
Most APIs won't need this, but in some cases (e.g. unsupported features), you can use escape hatches to drop out of
162+
the generated scaffolding.
163+
164+
For example, we pass the raw nextjs `request` object to your handler implementations,
165+
allowing you full control where its needed.
166+
```typescript
167+
export const GET = _GET(async ({params}, respond, request) => {
168+
console.log(request.nextUrl.buildId)
169+
// ...your implementation here
170+
})
171+
```
172+
173+
Use sparingly - the goal is to reduce the need for escape hatches over time.

packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export class TypescriptNextjsAppRouterBuilder implements ICompilable {
9898
}
9999

100100
innerFunction?.addParameter({name: "respond"})
101-
innerFunction?.addParameter({name: "context"})
101+
innerFunction?.addParameter({name: "request"})
102102
}
103103

104104
// TODO: duplication - should be shared with router builder

packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,7 @@ export async function generateTypescriptNextJS(
116116
)
117117
).flat()
118118

119-
const clientOutputPath = [generatedDirectory, "clients", "client.ts"].join(
120-
path.sep,
121-
)
119+
const clientOutputPath = [generatedDirectory, "client.ts"].join(path.sep)
122120
const clientImportBuilder = new ImportBuilder(
123121
{filename: clientOutputPath},
124122
importAlias,

0 commit comments

Comments
 (0)