openapi-domainify
v0.1.1
Published
Split OpenAPI specs into domain-based TypeScript services. Zero config required.
Maintainers
Readme
openapi-domainify
Split your OpenAPI spec into domain-based TypeScript services.
Most OpenAPI generators dump everything into a single file or force you to restructure your spec. This tool reads your existing spec and generates modular, domain-separated service classes based on URL path prefixes.
// Your config
domains: [
{ name: 'auth', prefix: '/auth/' },
{ name: 'billing', prefix: '/billing/' },
{ name: 'users', prefix: '/users/' },
]
// Generated usage
api.auth.login({ email, password })
api.billing.getInvoice(invoiceId)
api.users.updateProfile(userId, data)Why This Exists
Every OpenAPI TypeScript generator on npm does one of these:
| Tool | Approach | Problem |
|------|----------|---------|
| openapi-typescript | Types only | No service layer |
| openapi-fetch | Single client | One massive file |
| @hey-api/openapi-ts | Tag-based grouping | Requires spec changes |
| orval | React Query focused | Framework lock-in |
| openapi-generator | Mustache templates | 15MB dependency, Java heritage |
openapi-domainify uses path prefixes from your existing URLs. No spec modifications. No framework coupling. Just TypeScript.
Installation
npm install -D openapi-domainify
# or
pnpm add -D openapi-domainify
# or
bun add -D openapi-domainifyQuick Start
1. Create config file
npx openapi-domainify initThis creates domainify.config.ts:
import { defineConfig } from 'openapi-domainify'
export default defineConfig({
// OpenAPI spec source (URL or local file path)
input: 'https://api.example.com/openapi.json',
// Where to write generated code
output: './src/api',
// Domain splitting rules (order matters - first match wins)
domains: [
{ name: 'auth', prefix: '/auth/', className: 'Auth' },
{ name: 'users', prefix: '/users/', className: 'Users' },
],
// Catch-all for unmatched paths
fallback: { name: 'api', prefix: '/', className: 'Api' },
})2. Generate
npx openapi-domainify generateOutput structure:
src/api/
├── index.ts # API client (regenerated each time)
├── http.ts # HTTP client template (safe to edit)
├── generated/ # Auto-generated (DO NOT EDIT)
│ ├── openapi.json
│ ├── openapi.ts
│ ├── auth/
│ │ ├── service.ts
│ │ └── types.ts
│ └── users/
│ ├── service.ts
│ └── types.ts
└── overrides/ # Your customizations (safe to edit)
├── auth/
│ └── service.ts # Extend/override AuthService
└── users/
└── service.ts # Extend/override UsersServiceOverrides
When the generated code has issues or you need custom methods, create an override instead of editing generated files:
// overrides/users/service.ts
import { UsersService as GeneratedUsersService } from '../generated/users/service'
import type { HttpClient } from '../../http'
export class UsersService extends GeneratedUsersService {
constructor(http: HttpClient) {
super(http)
}
// Override a broken method
async getUser(id: string | number) {
// Custom implementation
return this.http.get(`/users/${id}`)
}
// Add a method not in the OpenAPI spec
async customEndpoint() {
return this.http.post('/users/custom')
}
}The generator:
- Never overwrites existing override files
- Always regenerates
generated/andindex.ts - Auto-detects if you've customized an override and uses it in
index.ts
Generated Code
Service Classes
// generated/users/service.ts
import type { HttpClient } from '../../http'
import type * as T from './types'
export class UsersService {
constructor(private http: HttpClient) {}
// GET /users
users(params?: T.GetUsersQuery): Promise<T.GetUsersResponse> {
return this.http.get('/users', { params })
}
// GET /users/{id}
getUser(id: string | number): Promise<T.GetUsersByIdResponse> {
return this.http.get(`/users/${id}`)
}
// POST /users
createUser(data: T.PostUsersRequest): Promise<T.PostUsersResponse> {
return this.http.post('/users', data)
}
}Type Extraction
Types are extracted directly from your OpenAPI spec using the TypeScript Compiler API:
// generated/users/types.ts
// Request types
export type PostUsersRequest = {
email: string
name: string
role?: 'admin' | 'user'
}
// Response types
export type GetUsersResponse = {
data: Array<{
id: number
email: string
name: string
}>
}HTTP Client
The generator creates an http.ts template with a FetchHttpClient implementation. Edit it or replace with your preferred HTTP library:
// http.ts (generated once, safe to edit)
export interface HttpClient {
get<T>(url: string, options?: { params?: Record<string, unknown> }): Promise<T>
post<T>(url: string, body?: Record<string, unknown>): Promise<T>
put<T>(url: string, body?: Record<string, unknown>): Promise<T>
patch<T>(url: string, body?: Record<string, unknown>): Promise<T>
delete<T>(url: string, options?: { params?: Record<string, unknown> }): Promise<T>
}Configuration Reference
import { defineConfig } from 'openapi-domainify'
export default defineConfig({
// Required: OpenAPI spec source
input: 'https://api.example.com/openapi.json',
// Required: Output directory
output: './src/api',
// Required: Domain splitting rules
domains: [
{
name: 'auth', // Directory name
prefix: '/auth/', // URL prefix to match
className: 'Auth' // Generated class name suffix
},
],
// Optional: Catch-all for unmatched paths
fallback: { name: 'api', prefix: '/', className: 'Api' },
// Optional: Strip this prefix from all paths
stripPrefix: '/api/v1',
// Optional: Where to save the downloaded spec
specOutput: './src/api/generated/openapi.json',
// Optional: Custom HTTP client import path
httpClientImport: '../../http',
// Optional: Override files directory (relative to output)
overridesDir: '../overrides',
// Optional: Generate index.ts (default: true)
generateIndex: true,
})Method Naming Convention
Generated method names follow RESTful conventions:
| HTTP Method | Path | Generated Method |
|-------------|------|------------------|
| GET | /users | users() |
| GET | /users/{id} | getUser(id) |
| POST | /users | createUser(data) |
| PUT | /users/{id} | updateUser(id, data) |
| PATCH | /users/{id} | patchUser(id, data) |
| DELETE | /users/{id} | deleteUser(id) |
| GET | /users/{id}/orders | getUserOrders(id) |
How It Works
- Fetch spec — Downloads OpenAPI JSON from URL or reads local file
- Generate types — Runs
openapi-typescriptto create base TypeScript types - Parse with TS Compiler API — Loads generated types into TypeScript's type checker
- Extract endpoints — Iterates over
pathsinterface, extracting methods, params, bodies - Group by domain — Matches each path against configured prefixes
- Generate services — Creates service classes with fully typed methods
- Generate scaffolding — Creates override templates (only if they don't exist)
- Wire up index — Generates
index.tsthat imports from overrides or generated
Comparison
| Feature | openapi-domainify | openapi-fetch | @hey-api/openapi-ts | orval | |---------|-------------------|---------------|---------------------|-------| | Domain splitting | ✅ Path prefix | ❌ | ⚠️ Tags only | ❌ | | No spec changes | ✅ | ✅ | ❌ | ✅ | | Override system | ✅ | ❌ | ❌ | ❌ | | Framework agnostic | ✅ | ✅ | ✅ | ❌ React Query | | Service classes | ✅ | ❌ | ✅ | ✅ |
License
MIT
