npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

km-api

v0.3.3

Published

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.

Readme

km-api

npm version License: MIT TypeScript Zod

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.

Features

  • Type-safe API definitions — full IntelliSense for every field
  • OpenAPI 3.0 compatible — path, methods, parameters, responses, examples
  • Dual path syntax — Express :param and 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

Installation

npm install km-api zod
# or
yarn add km-api zod
# or
pnpm add km-api zod
# or
bun add km-api zod

Compatibility

| 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+ |


Quick Start

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' }

API Reference

makeApiConfig(config)

Creates a typed endpoint configuration with helper methods attached.

Configuration fields

| 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 |

Returned helper methods

| 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 |


Path syntax

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'

Response schemas

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() }),
}

Response shape builders

Use makeResponseSuccessShape to create consistent wrappers for item and list responses.

Single item

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' } }

Paginated list

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)
// }

Custom metadata

const meta = z.object({ page: z.number(), total: z.number(), nextCursor: z.string().optional() });

const cursorListSchema = makeResponseSuccessShape(userSchema, 'users').list(meta);

HTTP client adapters

Response type conversion

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' }

Request body conversion

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 string

OpenAPI examples (optional)

Add 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: '[email protected]' },
      },
    },
    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 |


Authentication and headers

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',
});

Disabling endpoints

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');
}

Complete blog API example

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');

Testing with Vitest

Install test dependencies:

npm install --save-dev vitest
# or
bun add -d vitest

Configure 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' });
  });
});

Migration: v0.3.2 → v0.3.3

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 runtime

See rules.md for the full set of constraints that govern Zod usage in exported signatures.


Migration: v0.3.0 → v0.3.1

Renamed helper method

makeOpenAPIPath() has been renamed to makeOpenApiPathShape() for naming consistency.

// OLD (v0.3.0)
config.makeOpenAPIPath();

// NEW (v0.3.1)
config.makeOpenApiPathShape();

New helper method

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'

Migration: v0.2.x → v0.3.x

Import style

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.

Removed dependency

km-type is no longer required. Remove it from your package.json if you installed it separately.


Migration: v0.1.x → v0.2.x

Breaking changes

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() — removed
  • makeParamsString() — removed
  • makeFullPathShape() — removed
  • makeFullPath(params, orderList)makeFullPath(params) (no second argument)

Contributing

  1. Fork the repo
  2. Create your branch: git checkout -b feature/my-feature
  3. Run tests: npm test
  4. Submit a PR

License

MIT © komeilm76