A fully typed API client generator powered by OpenAPI.
Fetch-compatible, auto-generated types, zero generics required.
devup-api reads your openapi.json file and automatically generates a fully typed client that behaves like an ergonomic, type-safe version of fetch().
No manual type declarations. No generics. No SDK boilerplate.
Just write API calls β the types are already there.
- Reads
openapi.jsonand transforms every path, method, schema into typed API functions. - Parameters, request bodies, headers, responses β all typed automatically.
- No need to write or maintain separate TypeScript definitions.
devup-api feels like using fetch, but with superpowers:
- Path params automatically replaced
- Query/body/header types enforced
- Typed success & error responses
- Optional runtime schema validation
- Minimal abstraction over standard fetch
- Works seamlessly with Vite, Next.js, Webpack, and Rsbuild
- Automatic type generation during build time
- Zero runtime overhead
# For Vite projects
npm install @devup-api/fetch @devup-api/vite-plugin
# For Next.js projects
npm install @devup-api/fetch @devup-api/next-plugin
# For Webpack projects
npm install @devup-api/fetch @devup-api/webpack-plugin
# For Rsbuild projects
npm install @devup-api/fetch @devup-api/rsbuild-pluginVite (vite.config.ts):
import { defineConfig } from 'vite'
import devupApi from '@devup-api/vite-plugin'
export default defineConfig({
plugins: [devupApi()],
})Next.js (next.config.ts):
import devupApi from '@devup-api/next-plugin'
export default devupApi({
reactStrictMode: true,
})Webpack (webpack.config.js):
const { devupApiWebpackPlugin } = require('@devup-api/webpack-plugin')
module.exports = {
plugins: [new devupApiWebpackPlugin()],
}Rsbuild (rsbuild.config.ts):
import { defineConfig } from '@rsbuild/core'
import { devupApiRsbuildPlugin } from '@devup-api/rsbuild-plugin'
export default defineConfig({
plugins: [devupApiRsbuildPlugin()],
})Place your openapi.json file in the project root (or specify a custom path in plugin options).
Add the generated type definitions to your tsconfig.json:
{
"compilerOptions": {
// ... your compiler options
},
"include": [
"src",
"df/**/*.d.ts"
]
}Note: The
dfdirectory is the default temporary directory where generated types are stored. If you've customizedtempDirin plugin options, adjust the path accordingly (e.g.,"your-temp-dir/**/*.d.ts").
import { createApi } from '@devup-api/fetch'
const api = createApi('https://api.example.com')
// Use operationId
const users = await api.get('getUsers', {})
// Or use the path directly
const user = await api.get('/users/{id}', {
params: { id: '123' },
headers: {
Authorization: 'Bearer TOKEN'
}
})
// POST request with typed body
const newUser = await api.post('createUser', {
body: {
name: 'John Doe',
email: 'john@example.com'
}
})devup-api uses a two-phase typing system to ensure smooth development experience:
Cold typing refers to the state before the TypeScript interface files are generated. This happens when:
- You first install the plugin
- The build hasn't run yet
- The generated
api.d.tsfile doesn't exist
During cold typing:
- All API types are treated as
any - Type checking is relaxed to prevent type errors
- Your code will compile and run without issues
- You can write API calls without waiting for type generation
// Cold typing: No type errors even if api.d.ts doesn't exist yet
const api = createApi('https://api.example.com')
const result = await api.get('getUsers', {}) // β
Works, types are 'any'Bold typing refers to the state after the TypeScript interface files are generated. This happens when:
- The build tool has run (
devorbuild) - The plugin has generated
api.d.tsin the temp directory - TypeScript can find and use the generated types
During bold typing:
- All API types are strictly enforced
- Full type safety is applied
- Type errors will be caught at compile time
- You get full IntelliSense and autocomplete
// Bold typing: Full type safety after api.d.ts is generated
const api = createApi('https://api.example.com')
const result = await api.get('getUsers', {})
// β
Fully typed: result.data is typed based on your OpenAPI schema
// β Type error if you use wrong parameters or pathsThis two-phase approach ensures:
- No blocking: You can start coding immediately without waiting for the build
- Gradual typing: Types become available as soon as the build runs
- Production safety: Full type checking in production builds
- Developer experience: No false type errors during initial setup
This is a monorepo containing multiple packages:
@devup-api/core- Core types and interfaces@devup-api/utils- Utility functions for OpenAPI processing@devup-api/generator- TypeScript interface generator from OpenAPI schemas@devup-api/fetch- Type-safe API client@devup-api/react-query- TanStack React Query integration@devup-api/vite-plugin- Vite plugin@devup-api/next-plugin- Next.js plugin@devup-api/webpack-plugin- Webpack plugin@devup-api/rsbuild-plugin- Rsbuild plugin
// Using operationId
const users = await api.get('getUsers', {
query: { page: 1, limit: 20 }
})
// Using path
const users = await api.get('/users', {
query: { page: 1, limit: 20 }
})const newPost = await api.post('createPost', {
body: {
title: 'Hello World',
content: 'This is a typed API request.'
}
})const post = await api.get('/posts/{id}', {
params: { id: '777' }
})const result = await api.get('getUser', { params: { id: '123' } })
if (result.data) {
// Success response - fully typed!
console.log(result.data.name)
} else if (result.error) {
// Error response
console.error(result.error.message)
}DevupObject allows you to reference generated schema types directly, which is useful for typing variables, function parameters, or component props.
import { createApi, type DevupObject } from '@devup-api/fetch'
// Access response types from the default OpenAPI schema
type User = DevupObject['User']
type Product = DevupObject['Product']
// Use in your code
const user: User = {
id: '123',
name: 'John Doe',
email: 'john@example.com'
}
// For request/error types, specify the type category
type CreateUserRequest = DevupObject<'request'>['CreateUserBody']
type ApiError = DevupObject<'error'>['ErrorResponse']devup-api supports multiple OpenAPI schemas for working with different API servers.
Place multiple OpenAPI files in your project (e.g., openapi.json, openapi2.json) and the plugin will generate types for each.
import { createApi, type DevupObject } from '@devup-api/fetch'
// Default server (uses openapi.json)
const api = createApi({
baseUrl: 'https://api.example.com',
})
// Second server (uses openapi2.json)
const api2 = createApi({
baseUrl: 'https://api.another-service.com',
serverName: 'openapi2.json',
})
// Make requests to different servers
const users = await api.get('getUsers', {})
const products = await api2.get('getProducts', {})
// Access types from different schemas
type User = DevupObject['User'] // From openapi.json
type Product = DevupObject<'response', 'openapi2.json'>['Product'] // From openapi2.jsondevup-api provides first-class support for TanStack React Query through the @devup-api/react-query package.
npm install @devup-api/react-query @tanstack/react-queryimport { createApi } from '@devup-api/fetch'
import { createQueryClient } from '@devup-api/react-query'
const api = createApi('https://api.example.com')
const queryClient = createQueryClient(api)function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = queryClient.useQuery(
'get',
'/users/{id}',
{ params: { id: userId } }
)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <div>{data.name}</div>
}function CreateUser() {
const mutation = queryClient.useMutation('post', 'createUser')
return (
<button onClick={() => mutation.mutate({
body: { name: 'John', email: 'john@example.com' }
})}>
Create User
</button>
)
}function UserList() {
const { data } = queryClient.useSuspenseQuery('get', 'getUsers', {})
return <ul>{data.map(user => <li key={user.id}>{user.name}</li>)}</ul>
}function InfiniteUserList() {
const { data, fetchNextPage, hasNextPage } = queryClient.useInfiniteQuery(
'get',
'getUsers',
{
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextPage,
}
)
return (
<>
{data?.pages.map(page =>
page.users.map(user => <div key={user.id}>{user.name}</div>)
)}
{hasNextPage && <button onClick={() => fetchNextPage()}>Load More</button>}
</>
)
}All plugins accept the following options:
interface DevupApiOptions {
/**
* OpenAPI file path
* @default 'openapi.json'
*/
openapiFile?: string
/**
* Temporary directory for storing generated files
* @default 'df'
*/
tempDir?: string
/**
* Case conversion type for API endpoint names and parameters
* @default 'camel'
*/
convertCase?: 'snake' | 'camel' | 'pascal' | 'maintain'
/**
* Whether to make all properties non-nullable by default
* @default false
*/
requestDefaultNonNullable?: boolean
/**
* Whether to make all request properties non-nullable by default
* @default true
*/
responseDefaultNonNullable?: boolean
}- Plugin reads your
openapi.jsonduring build time - Extracts paths, methods, schemas, parameters, and request bodies
- Generates TypeScript interface definitions automatically
- Creates a URL map for operationId-based API calls
- Builds a typed wrapper around
fetch()with full type safety
# Install dependencies
bun install
# Build all packages
bun run build
# Run tests
bun test
# Lint
bun run lint
# Fix linting issues
bun run lint:fixThis project is inspired by openapi-fetch, a fantastic library for type-safe API clients. devup-api builds upon similar concepts while providing additional features like build-time type generation and seamless integration with modern build tools.
Apache 2.0