| name | devup-api |
| description | Type-safe API client generator from OpenAPI schemas with full ecosystem support.
TRIGGER WHEN:
- Setting up API client with OpenAPI schema (@devup-api/fetch)
- Making typed API requests (GET, POST, PUT, PATCH, DELETE)
- Using React Query hooks (@devup-api/react-query)
- Using Zod validation schemas (@devup-api/zod)
- Building forms with react-hook-form (@devup-api/hookform)
- Creating CRUD interfaces (@devup-api/ui)
- Configuring Vite/Next.js/Webpack/Rsbuild plugins
- Implementing authentication middleware
- Using DevupObject for type references
|
devup-api Usage Guide
Type-safe API client from OpenAPI. Zero generics, auto-generated types.
Setup
Install
npm install @devup-api/fetch @devup-api/vite-plugin
npm install @devup-api/fetch @devup-api/next-plugin
npm install @devup-api/fetch @devup-api/webpack-plugin
npm install @devup-api/fetch @devup-api/rsbuild-plugin
npm install @devup-api/react-query @tanstack/react-query
npm install @devup-api/zod zod
npm install @devup-api/hookform react-hook-form zod
npm install @devup-api/ui @tanstack/react-query react-hook-form zod
Configure Build Tool
import devupApi from '@devup-api/vite-plugin'
export default defineConfig({ plugins: [devupApi()] })
import devupApi from '@devup-api/next-plugin'
export default devupApi({ reactStrictMode: true })
tsconfig.json
{ "include": ["src", "df/**/*.d.ts"] }
Place openapi.json in project root, run npm run dev.
@devup-api/fetch — API Client
Create Client
import { createApi, type DevupObject } from '@devup-api/fetch'
const api = createApi('https://api.example.com')
const api = createApi({ baseUrl: 'https://api.example.com', headers: { 'X-Custom': 'value' } })
HTTP Methods
const users = await api.get('getUsers', { query: { page: 1, limit: 20 } })
const user = await api.get('/users/{id}', { params: { id: '123' } })
const created = await api.post('createUser', { body: { name: 'John', email: 'john@example.com' } })
await api.put('/users/{id}', { params: { id: '1' }, body: { name: 'Jane', email: 'jane@example.com' } })
await api.patch('/users/{id}', { params: { id: '1' }, body: { name: 'Jane' } })
await api.delete('/users/{id}', { params: { id: '1' } })
Response Handling
const result = await api.get('getUser', { params: { id: '1' } })
if (result.data) {
console.log(result.data.name)
} else if (result.error) {
console.error(result.error)
}
console.log(result.response.status)
DevupObject (Type References)
Use DevupObject directly in type annotations without redefining types:
const user: DevupObject['User'] = await fetchUser()
const body: DevupObject<'request'>['CreateUserBody'] = { name: 'John', email: 'john@example.com' }
const error: DevupObject<'error'>['ErrorResponse'] = result.error
function displayUser(user: DevupObject['User']) { }
function UserCard({ user }: { user: DevupObject['User'] }) { }
const product: DevupObject<'response', 'openapi2.json'>['Product'] = data
Middleware
api.use({
onRequest: async ({ request }) => {
const token = localStorage.getItem('token')
if (token) {
const headers = new Headers(request.headers)
headers.set('Authorization', `Bearer ${token}`)
return new Request(request, { headers })
}
}
})
api.use({
onResponse: async ({ request, response }) => {
if (response.status === 401) {
const newToken = await refreshToken()
const headers = new Headers(request.headers)
headers.set('Authorization', `Bearer ${newToken}`)
return fetch(new Request(request, { headers }))
}
}
})
@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.
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:
'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
import { createApi } from '@devup-api/fetch'
import { createQueryClient } from '@devup-api/react-query'
const api = createApi('https://api.example.com')
const queryClient = createQueryClient(api)
useQuery
const { data, isLoading, error, refetch } = queryClient.useQuery(
'get',
'/users/{id}',
{ params: { id: userId } },
{ staleTime: 5 * 60 * 1000 }
)
useMutation
const mutation = queryClient.useMutation('post', 'createUser', {
onSuccess: (data) => {
client.invalidateQueries({
queryKey: queryClient.getQueryKey('get', 'getUsers'),
})
}
})
mutation.mutate({ body: { name: 'John', email: 'john@example.com' } })
getQueryKey (Cache Invalidation)
Use queryClient.getQueryKey() to get type-safe query keys for cache invalidation:
queryClient.getQueryKey('get', 'getUsers')
queryClient.getQueryKey('get', '/users/{id}', { params: { id: '123' } })
client.invalidateQueries({
queryKey: queryClient.getQueryKey(
'get',
'/project-subject/projects/{projectId}/companion',
),
})
useSuspenseQuery
const { data } = queryClient.useSuspenseQuery('get', 'getUsers', {})
useInfiniteQuery
const { data, fetchNextPage, hasNextPage } = queryClient.useInfiniteQuery(
'get',
'getUsers',
{
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextPage
}
)
useQueries (Parallel)
const results = queryClient.useQueries([
['get', '/users/{id}', { params: { id: '1' } }],
['get', '/users/{id}', { params: { id: '2' } }],
])
@devup-api/zod — Runtime Validation
Schemas auto-generated from OpenAPI via virtual module.
import { schemas, responseSchemas, requestSchemas, errorSchemas, pathSchemas } from '@devup-api/zod'
const userSchema = responseSchemas.User
const createUserSchema = requestSchemas.CreateUserRequest
const errorSchema = errorSchemas.ApiError
const schema = pathSchemas.post['createUser']
const schema = pathSchemas.put['/users/{id}']
const productSchema = schemas['openapi2.json'].response.Product
const result = userSchema.safeParse(data)
if (result.success) {
console.log(result.data)
} else {
console.error(result.error.issues)
}
import { z } from 'zod'
type User = z.infer<typeof responseSchemas.User>
@devup-api/hookform — React Hook Form Integration
Auto-validation with Zod schemas from OpenAPI.
import { createApi } from '@devup-api/fetch'
import { ApiForm, useFormContext, useWatch, useFieldArray, Controller } from '@devup-api/hookform'
const api = createApi('https://api.example.com')
Basic Form
function FormFields() {
const { register, formState: { errors, isSubmitting } } = useFormContext()
return (
<>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
<input {...register('email')} type="email" />
{errors.email && <span>{errors.email.message}</span>}
<button type="submit" disabled={isSubmitting}>Submit</button>
</>
)
}
function CreateUserForm() {
return (
<ApiForm
api={api}
method="post"
path="createUser"
onSuccess={(data) => console.log('Created:', data)}
onError={(error) => console.error('Error:', error)}
onValidationError={(errors) => console.log('Validation:', errors)}
>
<FormFields />
</ApiForm>
)
}
Edit Form
<ApiForm
api={api}
method="put"
path="/users/{id}"
requestOptions={{ params: { id: '123' } }}
defaultValues={{ name: 'John', email: 'john@example.com' }}
mode="onChange"
resetOnSuccess
onSuccess={(data) => console.log('Updated:', data)}
>
<FormFields />
</ApiForm>
Props
| Prop | Type | Description |
|---|
api | DevupApi | API client |
method | 'post' | 'put' | 'patch' | 'delete' | HTTP method |
path | string | operationId or path |
requestOptions | { params?, query?, headers? } | Additional request options |
defaultValues | object | Form default values |
mode | 'onSubmit' | 'onBlur' | 'onChange' | Validation mode |
resetOnSuccess | boolean | Reset form after success |
onSuccess | (data) => void | Success callback |
onError | (error) => void | API error callback |
onValidationError | (errors) => void | Validation error callback |
@devup-api/ui — CRUD Components
Auto-generated CRUD from OpenAPI tags.
OpenAPI Tags
paths:
/users/{id}:
get:
tags: [devup:user:one]
put:
tags: [devup:user:edit]
patch:
tags: [devup:user:fix]
/users:
post:
tags: [devup:user:create]
Usage
import { createApi } from '@devup-api/fetch'
import { ApiCrud } from '@devup-api/ui'
import { crudConfigs } from '@devup-api/ui/crud'
const api = createApi('https://api.example.com')
Create Mode (no params)
<ApiCrud
config={crudConfigs.user}
api={api}
fields={[
{ name: 'name', label: 'Name', type: 'text', required: true },
{ name: 'email', label: 'Email', type: 'email', required: true },
]}
onCreateSuccess={(data) => console.log('Created:', data)}
/>
Edit Mode (with params)
<ApiCrud
config={crudConfigs.user}
api={api}
params={{ id: userId }}
editMode="fix"
fields={fields}
oneLoading={<div>Loading...</div>}
oneFallback={<div>Not found</div>}
onUpdateSuccess={(data) => console.log('Updated:', data)}
/>
Headless Mode (Render Function)
<ApiCrud config={crudConfigs.user} api={api} params={{ id: userId }}>
{({ form, mode, submit, isLoading, one }) => (
<form onSubmit={(e) => { e.preventDefault(); submit() }}>
<input {...form.register('name')} />
<input {...form.register('email')} />
<button disabled={isLoading}>
{mode === 'create' ? 'Create' : 'Save'}
</button>
</form>
)}
</ApiCrud>
Custom Renderers
<ApiCrud
config={crudConfigs.user}
api={api}
fields={fields}
renderField={(field, form) => (
<div key={field.name}>
<label>{field.label}</label>
<input {...form.register(field.name)} />
</div>
)}
renderSubmit={({ isLoading, mode }) => (
<button disabled={isLoading}>{mode === 'create' ? 'Create' : 'Update'}</button>
)}
/>
useApiCrud Hook
import { useApiCrud } from '@devup-api/ui'
const crud = useApiCrud({
config: crudConfigs.user,
api,
params: userId ? { id: userId } : undefined,
onCreateSuccess: (data) => console.log('Created:', data),
onUpdateSuccess: (data) => console.log('Updated:', data),
})
Field Types
text | number | email | password | url | tel | textarea | select | checkbox | radio | date | datetime | time | file | hidden | array | object
Multiple API Servers
devupApi({ openapiFiles: ['openapi.json', 'openapi2.json'] })
const api1 = createApi({ baseUrl: 'https://api1.com' })
const api2 = createApi({ baseUrl: 'https://api2.com', serverName: 'openapi2.json' })
const user: DevupObject['User'] = data
const product: DevupObject<'response', 'openapi2.json'>['Product'] = data
Plugin Options
interface DevupApiOptions {
openapiFiles?: string | string[]
tempDir?: string
convertCase?: 'snake' | 'camel' | 'pascal' | 'maintain'
requestDefaultNonNullable?: boolean
responseDefaultNonNullable?: boolean
serverActions?: boolean | {
enabled?: boolean
baseUrl?: string
}
}
Common Patterns
Request Cancellation
const controller = new AbortController()
setTimeout(() => controller.abort(), 5000)
await api.get('getUsers', { signal: controller.signal })
File Upload
const formData = new FormData()
formData.append('file', file)
await api.post('/upload', { body: formData })
Environment URL
const api = createApi(import.meta.env.VITE_API_URL || 'http://localhost:3000')
Troubleshooting
| 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 |