SKILL.md
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
# Core + Build Plugin (choose one)
npm install @devup-api/fetch @devup-api/vite-plugin # Vite
npm install @devup-api/fetch @devup-api/next-plugin # Next.js
npm install @devup-api/fetch @devup-api/webpack-plugin # Webpack
npm install @devup-api/fetch @devup-api/rsbuild-plugin # Rsbuild
# Optional Integrations
npm install @devup-api/react-query @tanstack/react-query # React Query
npm install @devup-api/zod zod # Zod validation
npm install @devup-api/hookform react-hook-form zod # Hook Form
npm install @devup-api/ui @tanstack/react-query react-hook-form zod # CRUD UI
Configure Build Tool
// vite.config.ts
import devupApi from '@devup-api/vite-plugin'
export default defineConfig({ plugins: [devupApi()] })
// next.config.ts
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')
// or with options
const api = createApi({ baseUrl: 'https://api.example.com', headers: { 'X-Custom': 'value' } })
HTTP Methods
// GET
const users = await api.get('getUsers', { query: { page: 1, limit: 20 } })
const user = await api.get('/users/{id}', { params: { id: '123' } })
// POST
const created = await api.post('createUser', { body: { name: 'John', email: '[email protected]' } })
// PUT / PATCH / DELETE
await api.put('/users/{id}', { params: { id: '1' }, body: { name: 'Jane', email: '[email protected]' } })
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) // typed response
} else if (result.error) {
console.error(result.error) // typed error
}
console.log(result.response.status) // raw Response
DevupObject (Type References)
Use DevupObject directly in type annotations without redefining types:
// Direct usage in variable declarations
const user: DevupObject['User'] = await fetchUser()
const body: DevupObject<'request'>['CreateUserBody'] = { name: 'John', email: '[email protected]' }
const error: DevupObject<'error'>['ErrorResponse'] = result.error
// Direct usage in function parameters
function displayUser(user: DevupObject['User']) { /* ... */ }
// Direct usage in component props
function UserCard({ user }: { user: DevupObject['User'] }) { /* ... */ }
// Multi-server types
const product: DevupObject<'response', 'openapi2.json'>['Product'] = data
Middleware
// Auth
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 })
}
}
})
// Token Refresh
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/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 } // React Query options
)
useMutation
const mutation = queryClient.useMutation('post', 'createUser', {
onSuccess: (data) => {
tanstackQueryClient.invalidateQueries({ queryKey: ['get', 'getUsers'] })
}
})
mutation.mutate({ body: { name: 'John', email: '[email protected]' } })
useSuspenseQuery
// Use with React Suspense
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'
// By category
const userSchema = responseSchemas.User
const createUserSchema = requestSchemas.CreateUserRequest
const errorSchema = errorSchemas.ApiError
// By path/operationId (for forms)
const schema = pathSchemas.post['createUser']
const schema = pathSchemas.put['/users/{id}']
// Multi-server
const productSchema = schemas['openapi2.json'].response.Product
// Validate
const result = userSchema.safeParse(data)
if (result.success) {
console.log(result.data)
} else {
console.error(result.error.issues)
}
// Type inference
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: '[email protected]' }}
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] # GET single (required)
put:
tags: [devup:user:edit] # PUT update
patch:
tags: [devup:user:fix] # PATCH update
/users:
post:
tags: [devup:user:create] # POST create (required)
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" // 'edit' (PUT) or 'fix' (PATCH)
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),
})
// crud.mode: 'create' | 'edit'
// crud.form: UseFormReturn
// crud.one: { data, isLoading, isError }
// crud.create: { mutate, isPending }
// crud.update: { mutate, isPending }
// crud.submit: () => void
// crud.isLoading: boolean
Field Types
text | number | email | password | url | tel | textarea | select | checkbox | radio | date | datetime | time | file | hidden | array | object
Multiple API Servers
// Plugin config
devupApi({ openapiFiles: ['openapi.json', 'openapi2.json'] })
// Usage
const api1 = createApi({ baseUrl: 'https://api1.com' })
const api2 = createApi({ baseUrl: 'https://api2.com', serverName: 'openapi2.json' })
// Types - use directly without redefining
const user: DevupObject['User'] = data // openapi.json
const product: DevupObject<'response', 'openapi2.json'>['Product'] = data // openapi2.json
Plugin Options
interface DevupApiOptions {
openapiFiles?: string | string[] // default: 'openapi.json'
tempDir?: string // default: 'df'
convertCase?: 'snake' | 'camel' | 'pascal' | 'maintain' // default: 'camel'
requestDefaultNonNullable?: boolean // default: false
responseDefaultNonNullable?: boolean // default: true
}
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 |
| 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 |