Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_4duOtJPsRLtmWBlDS9xSR.json
Original file line number Diff line number Diff line change
@@ -0,0 +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"}
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Just write API calls — the types are already there.
- [Packages](#-packages)
- [API Usage](#-api-usage)
- [Multiple API Servers](#-multiple-api-servers)
- [Next.js Server Actions](#-nextjs-server-actions)
- [React Query Integration](#-react-query-integration)
- [Advanced Usage](#-advanced-usage)
- [Configuration Options](#-configuration-options)
Expand Down Expand Up @@ -64,6 +65,11 @@ devup-api feels like using `fetch`, but with superpowers:
- Automatic type generation during build time
- Zero runtime overhead

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

---

## 🚀 Quick Start
Expand Down Expand Up @@ -714,6 +720,57 @@ type Product = DevupObject<'response', 'openapi2.json'>['Product'] // From open

---

## ⚡ Next.js Server Actions

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.

Set `serverActions.baseUrl` when generated actions should call a specific API origin:

```ts
// next.config.ts
import devupApi from '@devup-api/next-plugin'

export default devupApi({
reactStrictMode: true,
serverActions: {
baseUrl: 'https://api.example.com',
},
})
```

Then import generated actions from the virtual server module:

```tsx
'use client'

import { getUser } from '@devup-api/fetch/server'

export function UserButton() {
return (
<button
type="button"
onClick={async () => {
const result = await getUser({ params: { id: '123' } })
console.log(result.data)
console.log(result.response.status)
}}
>
Load user
</button>
)
}
```

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.

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.

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.

Server Actions are enabled by default. Disable generation explicitly with `serverActions: false` or `serverActions: { enabled: false }`.

---

## 🔄 React Query Integration

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.
Expand Down Expand Up @@ -1531,6 +1588,16 @@ interface DevupApiOptions {
* @default true
*/
responseDefaultNonNullable?: boolean

/**
* Generate operationId-based Server Action wrappers and expose them via
* @devup-api/fetch/server.
* @default true
*/
serverActions?: boolean | {
enabled?: boolean
baseUrl?: string
}
}
```

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

---

Expand Down
43 changes: 43 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,43 @@ api.use({

---

## @devup-api/fetch/server — Next.js Server Actions

Server Actions are generated by default. Set `serverActions.baseUrl` when generated actions should call a specific API origin.

```ts
// next.config.ts
import devupApi from '@devup-api/next-plugin'

export default devupApi({
reactStrictMode: true,
serverActions: {
baseUrl: 'https://api.example.com',
},
})
```

Use generated actions through the virtual module, never by importing `df/server.ts` directly:

```tsx
'use client'

import { getUser } from '@devup-api/fetch/server'

const result = await getUser({ params: { id: '123' } })
```

Notes:

- Generated `df/server.ts` contains `'use server'` and top-level named async exports.
- Generated actions return `DevupApiResponse<T, E, SerializedResponse>`.
- `@devup-api/fetch/server` has a cold typing fallback before `df` exists.
- The plugin aliases `@devup-api/fetch/server` to generated `df/server.ts` during dev/build.
- When enabled, every operationId is generated as a named Server Action export.
- Disable generation explicitly with `serverActions: false` or `serverActions: { enabled: false }`.

---

## @devup-api/react-query — React Query Hooks

```ts
Expand Down Expand Up @@ -495,6 +532,11 @@ interface DevupApiOptions {
convertCase?: 'snake' | 'camel' | 'pascal' | 'maintain' // default: 'camel'
requestDefaultNonNullable?: boolean // default: false
responseDefaultNonNullable?: boolean // default: true
// default: true; use false or { enabled: false } to disable
serverActions?: boolean | {
enabled?: boolean
baseUrl?: string
}
}
```

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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions examples/next/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { createApi, type DevupObject } from '@devup-api/fetch'
import { getUserById } from '@devup-api/fetch/server'
import { createQueryClient } from '@devup-api/react-query'
import { ApiCrud } from '@devup-api/ui'
import { schemas } from '@devup-api/zod'
Expand Down Expand Up @@ -113,6 +114,18 @@ export default function Home() {
<Box>
<ApiCrud api={'user'} apiClient={api} />
<Box>
<Box
onClick={() => {
getUserById({
params: { id: 1 },
query: { name: 'John Doe' },
}).then((res) => {
console.log(res)
})
}}
>
hello
</Box>
<Box>
<Box>
{(() => {
Expand Down
3 changes: 3 additions & 0 deletions examples/next/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const config = devupApi(
},
{
openapiFiles: ['./openapi.json', './openapi2.json', './openapi3.json'],
serverActions: {
baseUrl: 'https://api.example.com',
},
},
)

Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,16 @@ export interface DevupApiOptions extends DevupApiTypeGeneratorOptions {
* @default {'openapi.json'}
*/
openapiFiles?: string[] | string

/**
* Generate Server Action wrappers for operationId-based API calls.
*
* @default {true}
*/
serverActions?:
| boolean
| {
enabled?: boolean
baseUrl?: string
}
}
7 changes: 6 additions & 1 deletion packages/fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./server": {
"types": "./dist/server.d.ts",
"import": "./dist/server.js",
"require": "./dist/server.cjs"
}
},
"files": [
"dist"
],
"scripts": {
"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"
"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"
},
"publishConfig": {
"access": "public"
Expand Down
1 change: 1 addition & 0 deletions packages/fetch/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ test('index.ts exports', () => {
expect({ ...indexModule }).toEqual({
DevupApi: expect.any(Function),
createApi: expect.any(Function),
serializeApiResponse: expect.any(Function),
})
})
Loading