Skip to content

Commit a3e2150

Browse files
authored
Merge pull request #70 from dev-five-git/gen-server-code
Gen server code
2 parents 2dc4c7a + b1985bd commit a3e2150

33 files changed

Lines changed: 952 additions & 32 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"packages/webpack-plugin/package.json":"Patch","packages/fetch/package.json":"Patch","packages/next-plugin/package.json":"Patch","packages/vite-plugin/package.json":"Patch","packages/rsbuild-plugin/package.json":"Patch","packages/core/package.json":"Patch","packages/utils/package.json":"Patch","packages/generator/package.json":"Patch"},"note":"Gen server code","date":"2026-04-27T20:38:10.725587800Z"}

README.md

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Just write API calls — the types are already there.
3333
- [Packages](#-packages)
3434
- [API Usage](#-api-usage)
3535
- [Multiple API Servers](#-multiple-api-servers)
36+
- [Next.js Server Actions](#-nextjs-server-actions)
3637
- [React Query Integration](#-react-query-integration)
3738
- [Advanced Usage](#-advanced-usage)
3839
- [Configuration Options](#-configuration-options)
@@ -64,6 +65,11 @@ devup-api feels like using `fetch`, but with superpowers:
6465
- Automatic type generation during build time
6566
- Zero runtime overhead
6667

68+
### **⚡ Generated Next.js Server Actions**
69+
- Generate top-level Server Action functions from OpenAPI operationIds
70+
- Import actions from `@devup-api/fetch/server` instead of reaching into `df`
71+
- Cold typing works before generated files exist, then becomes fully typed after the plugin runs
72+
6773
---
6874

6975
## 🚀 Quick Start
@@ -714,6 +720,57 @@ type Product = DevupObject<'response', 'openapi2.json'>['Product'] // From open
714720
715721
---
716722
723+
## ⚡ Next.js Server Actions
724+
725+
devup-api generates named Server Action wrappers for operationId-based API calls by default. This is useful in Next.js App Router projects when you want to call server-side API functions from Client Components without manually writing one action per endpoint.
726+
727+
Set `serverActions.baseUrl` when generated actions should call a specific API origin:
728+
729+
```ts
730+
// next.config.ts
731+
import devupApi from '@devup-api/next-plugin'
732+
733+
export default devupApi({
734+
reactStrictMode: true,
735+
serverActions: {
736+
baseUrl: 'https://api.example.com',
737+
},
738+
})
739+
```
740+
741+
Then import generated actions from the virtual server module:
742+
743+
```tsx
744+
'use client'
745+
746+
import { getUser } from '@devup-api/fetch/server'
747+
748+
export function UserButton() {
749+
return (
750+
<button
751+
type="button"
752+
onClick={async () => {
753+
const result = await getUser({ params: { id: '123' } })
754+
console.log(result.data)
755+
console.log(result.response.status)
756+
}}
757+
>
758+
Load user
759+
</button>
760+
)
761+
}
762+
```
763+
764+
The generated `df/server.ts` file contains `'use server'` and exports one named async function for every operationId in your OpenAPI schemas. You should import from `@devup-api/fetch/server`, not from `df/server.ts` directly; the build plugin aliases that module to the generated file.
765+
766+
Generated actions return `DevupApiResponse<T, E, SerializedResponse>`. This keeps the same `data` / `error` / `isOk` / `isError` shape as normal `api.get()` calls, while replacing the native `Response` instance with a plain serializable response object that can cross the Server Action boundary.
767+
768+
During cold typing, `@devup-api/fetch/server` is still importable before `df` exists. The fallback keeps initial setup from failing, and the generated module replaces it with strict operation-specific types after `dev` or `build` runs.
769+
770+
Server Actions are enabled by default. Disable generation explicitly with `serverActions: false` or `serverActions: { enabled: false }`.
771+
772+
---
773+
717774
## 🔄 React Query Integration
718775

719776
devup-api provides first-class support for TanStack React Query through the `@devup-api/react-query` package. All hooks are fully typed based on your OpenAPI schema.
@@ -1531,6 +1588,16 @@ interface DevupApiOptions {
15311588
* @default true
15321589
*/
15331590
responseDefaultNonNullable?: boolean
1591+
1592+
/**
1593+
* Generate operationId-based Server Action wrappers and expose them via
1594+
* @devup-api/fetch/server.
1595+
* @default true
1596+
*/
1597+
serverActions?: boolean | {
1598+
enabled?: boolean
1599+
baseUrl?: string
1600+
}
15341601
}
15351602
```
15361603

@@ -1542,7 +1609,8 @@ interface DevupApiOptions {
15421609
2. Extracts paths, methods, schemas, parameters, and request bodies
15431610
3. Generates TypeScript interface definitions automatically
15441611
4. Creates a URL map for operationId-based API calls
1545-
5. Builds a typed wrapper around `fetch()` with full type safety
1612+
5. Generates named Server Actions in `df/server.ts` by default
1613+
6. Builds a typed wrapper around `fetch()` with full type safety
15461614

15471615
---
15481616

SKILL.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,43 @@ api.use({
150150

151151
---
152152

153+
## @devup-api/fetch/server — Next.js Server Actions
154+
155+
Server Actions are generated by default. Set `serverActions.baseUrl` when generated actions should call a specific API origin.
156+
157+
```ts
158+
// next.config.ts
159+
import devupApi from '@devup-api/next-plugin'
160+
161+
export default devupApi({
162+
reactStrictMode: true,
163+
serverActions: {
164+
baseUrl: 'https://api.example.com',
165+
},
166+
})
167+
```
168+
169+
Use generated actions through the virtual module, never by importing `df/server.ts` directly:
170+
171+
```tsx
172+
'use client'
173+
174+
import { getUser } from '@devup-api/fetch/server'
175+
176+
const result = await getUser({ params: { id: '123' } })
177+
```
178+
179+
Notes:
180+
181+
- Generated `df/server.ts` contains `'use server'` and top-level named async exports.
182+
- Generated actions return `DevupApiResponse<T, E, SerializedResponse>`.
183+
- `@devup-api/fetch/server` has a cold typing fallback before `df` exists.
184+
- The plugin aliases `@devup-api/fetch/server` to generated `df/server.ts` during dev/build.
185+
- When enabled, every operationId is generated as a named Server Action export.
186+
- Disable generation explicitly with `serverActions: false` or `serverActions: { enabled: false }`.
187+
188+
---
189+
153190
## @devup-api/react-query — React Query Hooks
154191

155192
```ts
@@ -495,6 +532,11 @@ interface DevupApiOptions {
495532
convertCase?: 'snake' | 'camel' | 'pascal' | 'maintain' // default: 'camel'
496533
requestDefaultNonNullable?: boolean // default: false
497534
responseDefaultNonNullable?: boolean // default: true
535+
// default: true; use false or { enabled: false } to disable
536+
serverActions?: boolean | {
537+
enabled?: boolean
538+
baseUrl?: string
539+
}
498540
}
499541
```
500542

@@ -531,6 +573,7 @@ const api = createApi(import.meta.env.VITE_API_URL || 'http://localhost:3000')
531573
| Issue | Solution |
532574
|-------|----------|
533575
| Types not appearing | Run `npm run dev`, check tsconfig includes `df/**/*.d.ts` |
576+
| Server Action import fails at runtime | Configure the build plugin so `@devup-api/fetch/server` aliases to generated `df/server.ts` |
534577
| operationId not found | Use path `/users/{id}` or verify openapi.json operationId |
535578
| Zod schemas empty | Ensure bundler plugin is configured, run dev server |
536579
| CRUD config missing | Add `devup:{name}:one` and `devup:{name}:create` tags to OpenAPI |

bun.lock

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/next/app/page.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import { createApi, type DevupObject } from '@devup-api/fetch'
4+
import { getUserById } from '@devup-api/fetch/server'
45
import { createQueryClient } from '@devup-api/react-query'
56
import { ApiCrud } from '@devup-api/ui'
67
import { schemas } from '@devup-api/zod'
@@ -113,6 +114,18 @@ export default function Home() {
113114
<Box>
114115
<ApiCrud api={'user'} apiClient={api} />
115116
<Box>
117+
<Box
118+
onClick={() => {
119+
getUserById({
120+
params: { id: 1 },
121+
query: { name: 'John Doe' },
122+
}).then((res) => {
123+
console.log(res)
124+
})
125+
}}
126+
>
127+
hello
128+
</Box>
116129
<Box>
117130
<Box>
118131
{(() => {

examples/next/next.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ const config = devupApi(
77
},
88
{
99
openapiFiles: ['./openapi.json', './openapi2.json', './openapi3.json'],
10+
serverActions: {
11+
baseUrl: 'https://api.example.com',
12+
},
1013
},
1114
)
1215

packages/core/src/options.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,16 @@ export interface DevupApiOptions extends DevupApiTypeGeneratorOptions {
2828
* @default {'openapi.json'}
2929
*/
3030
openapiFiles?: string[] | string
31+
32+
/**
33+
* Generate Server Action wrappers for operationId-based API calls.
34+
*
35+
* @default {true}
36+
*/
37+
serverActions?:
38+
| boolean
39+
| {
40+
enabled?: boolean
41+
baseUrl?: string
42+
}
3143
}

packages/fetch/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@
88
"types": "./dist/index.d.ts",
99
"import": "./dist/index.js",
1010
"require": "./dist/index.cjs"
11+
},
12+
"./server": {
13+
"types": "./dist/server.d.ts",
14+
"import": "./dist/server.js",
15+
"require": "./dist/server.cjs"
1116
}
1217
},
1318
"files": [
1419
"dist"
1520
],
1621
"scripts": {
17-
"build": "tsc && bun build --target node --outfile=dist/index.js src/index.ts --production --packages=external && bun build --target node --outfile=dist/index.cjs --format=cjs src/index.ts --production --packages=external"
22+
"build": "tsc && bun -e \"await Bun.write('dist/server.d.ts', await Bun.file('src/server.d.ts').text())\" && bun build --target node --outfile=dist/index.js src/index.ts --production --packages=external && bun build --target node --outfile=dist/index.cjs --format=cjs src/index.ts --production --packages=external && bun build --target node --outfile=dist/server.js src/server.ts --production --packages=external && bun build --target node --outfile=dist/server.cjs --format=cjs src/server.ts --production --packages=external"
1823
},
1924
"publishConfig": {
2025
"access": "public"

packages/fetch/src/__tests__/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ test('index.ts exports', () => {
77
expect({ ...indexModule }).toEqual({
88
DevupApi: expect.any(Function),
99
createApi: expect.any(Function),
10+
serializeApiResponse: expect.any(Function),
1011
})
1112
})

0 commit comments

Comments
 (0)