Type-safe API schema builder for TypeScript. Define HTTP endpoints with Zod validation, full OpenAPI 3.0 compatibility, and adapters for Axios, Fetch, and Alova.
- Type-safe API definitions — full IntelliSense for every field
- OpenAPI 3.0 compatible — path, methods, parameters, responses, examples
- Dual path syntax — Express
:paramand OpenAPI{param}both work - Zod schema validation — runtime-safe request and response data
- HTTP client adapters — Axios, Fetch, Alova (UniApp, XHR, Taro)
- Request body conversion — auto-converts JSON, form-data, multipart, binary
- Response shape builders — standardised single-item and paginated-list wrappers
- OpenAPI examples — optional per-endpoint request/response examples
npm install km-api zod
# or
yarn add km-api zod
# or
pnpm add km-api zod
# or
bun add km-api zod| km-api | TypeScript | Zod | Node.js |
|---|---|---|---|
| 0.3.x | 5.9+ | 4.x | 14+ |
| 0.2.x | 5.9+ | 4.x | 14+ |
| 0.1.x | 5.x | 3.x | 14+ |
import { z } from 'zod';
import { makeApiConfig } from 'km-api';
const getUser = makeApiConfig({
method: 'GET',
pathShape: '/users/{id}',
tags: ['#users'],
auth: 'YES',
responseContentType: 'application/json',
summary: 'Get user by ID',
description: 'Retrieves a single user by their unique identifier.',
request: {
body: z.any(),
params: z.object({ id: z.string().uuid() }),
query: z.object({ include: z.enum(['profile', 'settings']).optional() }),
headers: z.object({ 'x-api-key': z.string().optional() }),
cookies: z.object({ sessionId: z.string().optional() }),
},
response: {
200: z.object({ id: z.string(), name: z.string(), email: z.string() }),
404: z.object({ message: z.string() }),
},
});
// Resolve path
getUser.makeFullPath({ id: '550e8400-e29b-41d4-a716-446655440000' });
// → '/users/550e8400-e29b-41d4-a716-446655440000'
// OpenAPI path format
getUser.makeOpenApiPathShape();
// → '/users/{id}'
// Express path format
getUser.makeExpressPathShape();
// → '/users/:id'
// HTTP client adapter config
getUser.convertResponseType('axios'); // { responseType: 'json' }
getUser.convertResponseType('fetch'); // { responseMethod: 'json' }Creates a typed endpoint configuration with helper methods attached.
| Field | Type | Required | Description |
|---|---|---|---|
method |
IMethod |
✅ | HTTP method (case-insensitive) |
pathShape |
IPath |
✅ | Path starting with / |
request.body |
ZodType |
✅ | Request body Zod schema |
request.params |
ZodObject |
✅ | Path parameters Zod schema |
request.query |
ZodObject |
✅ | Query parameters Zod schema |
request.headers |
ZodObject |
✅ | Custom headers Zod schema |
request.cookies |
ZodObject |
✅ | Cookie parameters Zod schema |
response |
Record<statusCode, ZodType> |
✅ | Response schemas keyed by status code |
tags |
string[] |
– | Tags prefixed with # |
auth |
'YES' | 'NO' |
– | Authentication requirement |
responseContentType |
IResponseContentType |
– | Response MIME type |
requestContentType |
IRequestContentType |
– | Request body MIME type |
disable |
'YES' | 'NO' |
– | Mark endpoint as disabled |
summary |
string |
– | Short summary (1–2 sentences) |
description |
string |
– | Detailed description (Markdown) |
examples |
IEndpointExamples |
– | OpenAPI 3.0 examples for docs |
| Method | Signature | Description |
|---|---|---|
makeBody |
(data) => data |
Returns type-safe request body |
makeParams |
(params) => params |
Returns type-safe path parameters |
makeQueries |
(queries) => queries |
Returns type-safe query parameters |
makeHeaders |
(headers) => headers |
Returns type-safe headers |
makeCookies |
(cookies) => cookies |
Returns type-safe cookies |
makeFullPath |
(params) => string |
Resolves the path template to a URL |
makeOpenApiPathShape |
() => string |
Converts path to OpenAPI {param} format |
makeExpressPathShape |
() => string |
Converts path to Express :param format |
convertResponseType |
(adapter) => AdapterConfig |
Returns adapter-specific response config |
Both path parameter styles are supported and can be mixed:
// Express-style
pathShape: '/users/:id/posts/:postId'
// OpenAPI-style
pathShape: '/users/{id}/posts/{postId}'
// Mixed (valid)
pathShape: '/users/:id/posts/{postId}'config.makeFullPath({ id: '1', postId: '42' }); // '/users/1/posts/42'
config.makeOpenApiPathShape(); // '/users/{id}/posts/{postId}'
config.makeExpressPathShape(); // '/users/:id/posts/:postId'Map status codes to Zod schemas. Both number and string keys are accepted:
response: {
200: z.object({ id: z.string(), name: z.string() }),
201: z.object({ id: z.string() }), // alternate success
400: z.object({ message: z.string() }),
'404': z.object({ message: z.string() }), // string key also works
500: z.object({ error: z.string() }),
}Use makeResponseSuccessShape to create consistent wrappers for item and list responses.
import { makeResponseSuccessShape } from 'km-api';
const userSchema = z.object({ id: z.string(), name: z.string() });
const shape = makeResponseSuccessShape(userSchema, 'user');
const itemSchema = shape.item();
// Validates: { user: { id: '1', name: 'Alice' } }import { makeResponseSuccessShape, paginationSchema } from 'km-api';
const listSchema = makeResponseSuccessShape(userSchema, 'users')
.list(paginationSchema());
// Validates:
// {
// users: [{ id: '1', name: 'Alice' }, ...],
// currentPage: 1,
// totalItems: 50,
// itemsPerPage: 20,
// totalPages: 3 (optional)
// }const meta = z.object({ page: z.number(), total: z.number(), nextCursor: z.string().optional() });
const cursorListSchema = makeResponseSuccessShape(userSchema, 'users').list(meta);const config = makeApiConfig({ responseContentType: 'application/pdf', ... });
// Axios
config.convertResponseType('axios'); // { responseType: 'blob' }
// Fetch API
config.convertResponseType('fetch'); // { responseMethod: 'blob' }
// Alova variants
config.convertResponseType('alova-axios'); // { responseType: 'blob' }
config.convertResponseType('alova-uniapp'); // { responseType: 'arraybuffer' }
config.convertResponseType('alova-xhr'); // { responseType: 'blob' }
config.convertResponseType('alova-taro'); // { responseType: 'arraybuffer', dataType: 'arraybuffer' }import { convertRequestBody, safeConvertRequestBody } from 'km-api';
// JSON
convertRequestBody({ name: 'Alice' }, 'application/json');
// → '{"name":"Alice"}'
// Form URL-encoded
convertRequestBody({ q: 'search term' }, 'application/x-www-form-urlencoded');
// → URLSearchParams { 'q' => 'search term' }
// Multipart
convertRequestBody({ file: myBlob, title: 'photo' }, 'multipart/form-data');
// → FormData instance
// safeConvertRequestBody — only converts if needed
safeConvertRequestBody('{"name":"Alice"}', 'application/json'); // returned unchanged
safeConvertRequestBody({ name: 'Alice' }, 'application/json'); // converted to stringAdd examples to any endpoint for documentation tools that support OpenAPI 3.0 examples:
const createUser = makeApiConfig({
method: 'POST',
pathShape: '/users',
requestContentType: 'application/json',
responseContentType: 'application/json',
request: {
body: z.object({ name: z.string(), email: z.string().email() }),
params: z.object({}),
query: z.object({}),
headers: z.object({}),
cookies: z.object({}),
},
response: {
201: z.object({ id: z.string(), name: z.string() }),
422: z.object({ message: z.string() }),
},
examples: {
request: {
alice: {
summary: 'Create Alice',
value: { name: 'Alice', email: 'alice@example.com' },
},
},
response: {
'201': {
created: {
summary: 'User created successfully',
value: { id: 'uuid-here', name: 'Alice' },
},
},
'422': {
invalidEmail: {
summary: 'Invalid email address',
value: { message: 'email: Invalid email' },
},
},
},
},
});Each example follows the OpenAPI 3.0 Example Object:
| Field | Type | Description |
|---|---|---|
summary |
string |
Short description of the example |
description |
string |
Long description, Markdown supported |
value |
unknown |
The example value (mutually exclusive with externalValue) |
externalValue |
string |
URL to an external example file |
const protectedEndpoint = makeApiConfig({
method: 'GET',
pathShape: '/admin/users',
auth: 'YES',
request: {
body: z.any(),
params: z.object({}),
query: z.object({}),
headers: z.object({
Authorization: z.string(),
'x-tenant-id': z.string().uuid(),
}),
cookies: z.object({}),
},
response: {
200: z.array(z.object({ id: z.string() })),
401: z.object({ message: z.string() }),
403: z.object({ message: z.string() }),
},
});
const headers = protectedEndpoint.makeHeaders({
Authorization: 'Bearer token123',
'x-tenant-id': '550e8400-e29b-41d4-a716-446655440000',
});const legacyEndpoint = makeApiConfig({
method: 'GET',
pathShape: '/v1/users',
disable: 'YES',
description: 'Deprecated. Use /v2/users instead.',
request: { ... },
response: { ... },
});
if (legacyEndpoint.disable === 'YES') {
console.warn('This endpoint is disabled');
}import { z } from 'zod';
import { makeApiConfig, makeResponseSuccessShape, paginationSchema } from 'km-api';
const postSchema = z.object({
id: z.string().uuid(),
title: z.string(),
content: z.string(),
authorId: z.string().uuid(),
createdAt: z.string().datetime(),
});
const errorSchema = z.object({ message: z.string(), code: z.number().int() });
const blogApi = {
listPosts: makeApiConfig({
method: 'GET',
pathShape: '/posts',
auth: 'NO',
responseContentType: 'application/json',
summary: 'List all posts',
request: {
body: z.any(),
params: z.object({}),
query: z.object({
page: z.number().int().min(1).optional(),
limit: z.number().int().max(100).optional(),
tag: z.string().optional(),
}),
headers: z.object({}),
cookies: z.object({}),
},
response: {
200: makeResponseSuccessShape(postSchema, 'posts').list(paginationSchema()),
},
}),
getPost: makeApiConfig({
method: 'GET',
pathShape: '/posts/{slug}',
auth: 'NO',
responseContentType: 'application/json',
summary: 'Get post by slug',
request: {
body: z.any(),
params: z.object({ slug: z.string() }),
query: z.object({}),
headers: z.object({}),
cookies: z.object({}),
},
response: {
200: makeResponseSuccessShape(postSchema, 'post').item(),
404: errorSchema,
},
}),
createPost: makeApiConfig({
method: 'POST',
pathShape: '/posts',
auth: 'YES',
requestContentType: 'application/json',
responseContentType: 'application/json',
summary: 'Create a new post',
tags: ['#posts', '#write'],
request: {
body: z.object({
title: z.string().min(3).max(200),
content: z.string().min(10),
tags: z.array(z.string()).optional(),
}),
params: z.object({}),
query: z.object({}),
headers: z.object({ Authorization: z.string() }),
cookies: z.object({}),
},
response: {
201: makeResponseSuccessShape(postSchema, 'post').item(),
400: errorSchema,
401: errorSchema,
},
}),
deletePost: makeApiConfig({
method: 'DELETE',
pathShape: '/posts/{id}',
auth: 'YES',
summary: 'Delete a post',
request: {
body: z.any(),
params: z.object({ id: z.string().uuid() }),
query: z.object({}),
headers: z.object({ Authorization: z.string() }),
cookies: z.object({}),
},
response: {
204: z.object({}),
403: errorSchema,
404: errorSchema,
},
}),
};
// Usage
const listQuery = blogApi.listPosts.makeQueries({ page: 2, limit: 10 });
const postPath = blogApi.getPost.makeFullPath({ slug: 'hello-world' });
const axiosCfg = blogApi.createPost.convertResponseType('axios');Install test dependencies:
npm install --save-dev vitest
# or
bun add -d vitestConfigure Vitest (vitest.config.ts):
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
},
});Write tests against your endpoint configs:
import { describe, it, expect } from 'vitest';
import { getUser } from './api/users';
describe('getUser', () => {
it('resolves path correctly', () => {
expect(getUser.makeFullPath({ id: '123' })).toBe('/users/123');
});
it('converts to OpenAPI path', () => {
expect(getUser.makeOpenApiPathShape()).toBe('/users/{id}');
});
it('converts to Express path', () => {
expect(getUser.makeExpressPathShape()).toBe('/users/:id');
});
it('returns axios config for JSON', () => {
expect(getUser.convertResponseType('axios')).toEqual({ responseType: 'json' });
});
it('validates params schema', () => {
const validParams = getUser.request.params.parse({ id: '550e8400-e29b-41d4-a716-446655440000' });
expect(validParams).toEqual({ id: '550e8400-e29b-41d4-a716-446655440000' });
});
});No API changes. This release replaces Zod types in all exported generic constraints and return types with local structural types, eliminating the TypeScript Language Server hang that some IDEs experienced on import.
If you were casting the result of makeResponseSuccessShape().item() or
.list() to ZodObject, replace it with the structural output type instead:
// v0.3.2 — inferred as ZodObject<{...}>
const schema = makeResponseSuccessShape(userSchema, 'user').item();
// v0.3.3 — inferred as { readonly _zod: { readonly output: { user: ... } } }
// runtime value is still a real ZodObject — call .parse() freely
const schema = makeResponseSuccessShape(userSchema, 'user').item();
schema.parse({ user: { id: '1', name: 'Alice' } }); // still works at runtimeSee rules.md for the full set of constraints that govern
Zod usage in exported signatures.
makeOpenAPIPath() has been renamed to makeOpenApiPathShape() for naming consistency.
// OLD (v0.3.0)
config.makeOpenAPIPath();
// NEW (v0.3.1)
config.makeOpenApiPathShape();makeExpressPathShape() converts any path to Express :param format:
// pathShape: '/users/{id}/posts/{postId}'
config.makeExpressPathShape(); // '/users/:id/posts/:postId'
// pathShape: '/users/:id' (already Express-style — unchanged)
config.makeExpressPathShape(); // '/users/:id'v0.2.x used a deeply nested namespace:
// OLD (v0.2.x)
import { kmApi } from 'km-api';
kmApi.apiConfig.makeApiConfig({ ... });v0.3.x uses flat exports:
// NEW (v0.3.x)
import { makeApiConfig } from 'km-api';
makeApiConfig({ ... });All names are the same — only the import path changed.
km-type is no longer required. Remove it from your package.json if you installed it separately.
path renamed to pathShape
// v0.1.x
{ path: '/users/{id}' }
// v0.2.x+
{ pathShape: '/users/{id}' }Response object restructured
// v0.1.x
response: {
success: z.object({ ... }),
error: z.object({ ... }),
}
// v0.2.x+
response: {
200: z.object({ ... }),
400: z.object({ ... }),
}Removed functions
makeParamsOrderedList()— removedmakeParamsString()— removedmakeFullPathShape()— removedmakeFullPath(params, orderList)→makeFullPath(params)(no second argument)
- Fork the repo
- Create your branch:
git checkout -b feature/my-feature - Run tests:
npm test - Submit a PR
MIT © komeilm76