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

@weifuwujs/api

v1.3.0

Published

weifuwu 单源 API:一份类型描述符同时生成 REST 路由与 GraphQL schema(graphql-js 引擎)

Downloads

815

Readme

单源 API:REST + GraphQL(@weifuwujs/api)

一份类型描述符同时生成 REST 路由与 GraphQL schema;依赖随包走(graphql 为 peer)。

同一份类型描述与业务逻辑,同时投影出 REST 路由和 GraphQL schema。 独立包,不 import 不背依赖(仅使用方自带 graphql peer):

import { defineApi, defineResource, t } from '@weifuwujs/api'

最小可跑示例(零依赖,node 直接运行;test/batteries/readme-examples.test.ts 会真跑它):

import assert from 'node:assert/strict'
import { createApp } from '@weifuwujs/weifuwu'
import { defineApi, t } from '@weifuwujs/api'

const Ping = t.object('Ping', { ok: t.boolean() })
const api = defineApi()
  .query('ping', { returns: Ping, rest: 'GET /ping' }, () => ({ ok: true }))
  .build()

const app = createApp().mount(api, { openapi: '/openapi.json' })
const response = await app.fetch(new Request('http://readme/ping'))
assert.equal(response.status, 200)
assert.deepEqual(await response.json(), { ok: true })

示例标注:ts exec = 由测试真跑;ts fragment = 片段(依赖前后文,不独立运行);自足示例带包标签(如 ts tasks),由测试编译。

GraphQL 的 schema 构建与执行由 graphql + @graphql-tools/schema 完成 (graphql 是 peer dependency):解析、校验、执行都是标准实现。

类型描述符

const Role = t.enum('Role', ['admin', 'member'] as const)

const User = t.object('User', {
  id: t.id(),
  name: t.string(),
  email: t.string().nullable(),
  role: Role,
  tags: t.list(t.string()),
})

const CreateUserInput = t.input('CreateUserInput', {
  name: t.string(),
  email: t.string().optional(),
})

| 工厂 | GraphQL | TypeScript | | --- | --- | --- | | t.id() | ID | string | | t.string() / t.int() / t.float() / t.boolean() | 对应内置 scalar | string / number / number / boolean | | t.json<T>() / t.datetime() / t.bigint() | 自定义 scalar | T / Date / bigint | | t.enum(name, values) | enum | 字面量联合 | | t.object(name, fields) / t.input(name, fields) | type / input | 结构化对象 | | t.list(item) | [Item] | T[] | | t.lazy(() => Type) | 延迟解析,用于递归类型 | 同内层类型 |

修饰符(返回新描述符,不改原值):.nullable()(GraphQL 可空)、 .optional()(参数/输入可省)、.default(v)、.describe(text)。

推导:Infer<typeof User> 得到 { id: string; name: string; email: string | null; ... }, 操作参数的 .optional() / .default() 会自动变成可选键。

定义操作

const api = defineApi<Context>()
  .query(
    'user',
    { returns: User.nullable(), args: { id: t.id() }, rest: 'GET /users/:id' },
    ({ args, ctx }) => ctx.db.users.find(args.id), // args.id: string
  )
  .mutation(
    'createUser',
    {
      returns: User,
      args: { input: CreateUserInput },
      rest: { method: 'POST', path: '/users', status: 201 },
    },
    ({ args, ctx }) => ctx.db.users.create(args.input),
  )
  .build()

createApp().mount(api, { graphql: '/graphql' })
  • kind 由 query() / mutation() 决定,对应 GraphQL 根类型。
  • rest:'GET /users/:id' 或 { method, path, status? };false 表示仅 GraphQL。
  • resolve 接收 { args, ctx, req, operation, info? };resolver 里可用 ctx.req 拿到当前请求。 info 仅在 GraphQL 路径提供(graphql-js GraphQLResolveInfo,含选择集),REST 路径为 undefined—— 字段级授权据此区分「未请求字段」(可裁剪)与「显式请求越权字段」(须拒绝)。

CRUD 语法糖

const users = defineResource<Context>()({
  type: User,
  path: '/users',
  createInput: CreateUserInput,
  updateInput: CreateUserInput,
  handlers: { list, get, create, update, remove },
})

const api = defineApi().use(users).build()

自动生成 GET /users、GET /users/:id、POST /users(201)、PATCH /users/:id、 DELETE /users/:id,以及 GraphQL 的 users / user(id) / createUser(input) / updateUser(id, input) / deleteUser(id);未提供 handler 的方法不会生成。

类型推断提示:TypeScript 不支持部分类型参数推断。需要自定义 Ctx 时用柯里化形态 defineResource<Ctx>()({ ... }):第一层只绑定 Ctx,配置里的 type、 createInput、updateInput 仍会完整推断,handler 的 args / ctx 保持精确类型。 若直接写 defineResource<Ctx>({ ... }),其余泛型会退回默认值,handler 参数与返回值 退化为 any(不会有编译错误,静默失去类型保护);只有不需要精确类型时才这样写。 Ctx 就用默认值(Context)时,直接 defineResource({ ... }) 即可,无需柯里化。

资源类型进 ctx(typedResource)

内核资源(resource() / postgres() / redis())注入的 ctx[key] 类型可以直接带进 resolver, 不用手写 defineApi<AppContext>():

import { resource } from '@weifuwujs/weifuwu'
import { defineApi, t, typedResource } from '@weifuwujs/api'

interface User {
  id: string
  name: string
}

const UserType = t.object('User', { id: t.id(), name: t.string() })
const db = resource('db', () => ({ users: [] as User[] }))

export const api = defineApi()
  .use(typedResource(db))
  .query('users', { returns: t.list(UserType), rest: 'GET /users' }, ({ ctx }) => ctx.db.users)
  .build()

typedResource 是纯类型载体:资源仍必须挂载(app.use(db)), 懒初始化、single-flight 与 app.close() 清理都归内核资源中间件; 只在 api 里声明、没 app.use → resolver 里 ctx.db 是 undefined(HTTP 500),不会自动注入。

import { createApp, resource } from '@weifuwujs/weifuwu'
import { defineApi, typedResource } from '@weifuwujs/api'

const ping = resource('ping', () => 'pong')
const api = defineApi().use(typedResource(ping)).build()

// 资源中间件挂载在 app 上(typedResource 不做注入):app.use(ping).mount(api)
export const app = createApp().use(ping).mount(api)

没有 typedResource 声明的资源字段在编译期是 unknown(Context 有索引签名,所以「能读不能用」): 直接 ctx.db.users 会报类型错,必须先显式收窄——拼错 key 不会静默变成 any。

REST 映射规则

| 来源 | 规则 | | --- | --- | | 路径参数 | :id → args.id,按描述符转换 | | 请求体 | 按参数名取字段;若 body 里没有任何参数名,则整体作为唯一的非路径参数 | | 查询参数 | GET/DELETE 未在路径/体中找到的参数从 ctx.query 取 | | 校验失败 | 422 { errors: [{ path, message }] } | | 返回值 | JSON;HttpError 映射为对应状态,其他异常为 500 | | 状态码 | 默认 200,可在 rest.status 指定 |

GraphQL 端点

app.mount(api, { graphql: '/graphql' }) 同时注册 GET 与 POST:

curl -H 'Content-Type: application/json' \
  -d '{"query":"query($id: ID!){ user(id: $id){ id name } }","variables":{"id":"1"}}' \
  http://localhost:3000/graphql
  • 执行由 graphql-js 提供:query / mutation、变量与默认值、别名、嵌套选择、 命名/内联片段、@skip / @include、__typename、introspection、错误 path、 非空向上传播。
  • schema 由描述符生成,只包含 object / input / enum / scalar 与 Query / Mutation (没有 union / interface / subscription)。
  • t.json() 等自定义 scalar 在查询中不要写子选择集:合法写法是 debug, 而不是 debug { ... }。
  • 校验失败:200 + errors,且不含部分 data(GraphQL 规范); 语法错误 400;GET 被禁用 405;非 JSON Content-Type 415。
  • 错误脱敏与 HTTP 同口径:HttpError 默认 expose = status < 500, 未暴露的错误在响应里只给状态文案(如 500 → Internal Server Error) 并保留 extensions.status;原始错误传给 graphqlHandler({ onError }), 默认打印 console.error(可接结构化日志,例如: api.graphqlHandler({ onError: (error) => logger.error(error) }))。

需要 SDL 或自行组合时:

const { sdl, rootResolvers } = api.schema()
// 或
const sdlText = api.sdl()

挂载选项与分组

app.mount(api, { rest: true, graphql: '/graphql' })
app.mount(api, { openapi: '/openapi.json' })                                     // REST + OpenAPI
app.group('/v1', (group) => group.mount(api, { graphql: '/graphql' }))            // /v1/graphql

完整可运行示例见 examples/rest-graphql.ts(REST + GraphQL)与 examples/openapi.ts(REST + GraphQL + OpenAPI)。

OpenAPI 3.1 文档

一行挂载即得到 REST 投影的机器可读契约(与 GraphQL 的 api.sdl() 对称):

import { createApp } from '@weifuwujs/weifuwu'
import { defineApi, t } from '@weifuwujs/api'

const Ping = t.object('Ping', { ok: t.boolean() })
const api = defineApi()
  .query('ping', { returns: Ping, rest: 'GET /ping' }, () => ({ ok: true }))
  .build()

const app = createApp().mount(api, { openapi: '/openapi.json' })

// 或自行取文档 / handler:
export const document = api.openapi({ title: 'Ping API', version: '1.0.0' })
export const handler = api.openapiHandler({ title: 'Ping API', version: '1.0.0' })
  • 文档由同一份类型描述符生成:路径与方法、路径/查询参数、requestBody、成功与 400/422/500 响应、 components.schemas(object / input / enum)都来自 defineApi / defineResource 的定义; rest: false 的纯 GraphQL 操作不会出现。
  • 生成是确定的:同一 API 两次生成的 JSON.stringify 逐字节一致(paths 按声明序、schemas 按名字典序)。
  • group.mount(api, { openapi: '/openapi.json' }) 前缀生效;提供了 path 但 API 没有 handler 时显式报错。

交给工具链(Postman / Swagger Editor / Insomnia 可直接导入 URL):

curl http://localhost:3000/openapi.json -o openapi.json
npx --yes openapi-typescript@7 openapi.json -o api-types.d.ts   # 生成客户端类型

monorepo/workspace 里 npx 会把相对输入挂到 npm local prefix(通常是包根):用绝对路径 (npx --yes openapi-typescript@7 "$PWD/openapi.json" -o "$PWD/api-types.d.ts")最稳。

BigInt 与运行时宽容度

t.bigint() 的 JSON 表示统一为 decimal string(超出 JS 安全整数不丢精度):REST 响应与 GraphQL 数据都是 "9007199254740993",输入接受 string / number / bigint;文档中 schema 为 { type: 'string', pattern: '^-?[0-9]+$' }。

文档描述推荐(canonical)形态;REST 运行时还接受这些宽容形态(不在 spec 内):

| 宽容形态 | 示例 | | --- | --- | | 数字 ID | GET /users/42(路径/查询参数本质是字符串) | | 布尔字符串 | ?flag=true / ?flag=false | | 单值列表 | body {"tags":"a"} 等价 {"tags":["a"]} | | 整体 body | 恰有一个非路径参数时,body 可直接是该参数值(spec 的 oneOf 同时描述两种形态) |

import { createApp, type Context } from '@weifuwujs/weifuwu'
import { defineApi, defineResource, t } from '@weifuwujs/api'

const User = t.object('User', { id: t.id(), name: t.string() })
const CreateUserInput = t.input('CreateUserInput', { name: t.string() })

// 自定义上下文用柯里化:显式绑定 Ctx,同时保留 type / createInput 的推断
type AppContext = Context & { db: { name: string } }

const users = defineResource<AppContext>()({
  type: User,
  path: '/users',
  createInput: CreateUserInput,
  handlers: {
    list: ({ ctx }) => [{ id: '1', name: ctx.db.name }],
    get: ({ args }) => ({ id: args.id, name: 'Ada' }),
  },
})

const api = defineApi().use(users).build()

const app = createApp().mount(api, { graphql: '/graphql', openapi: '/openapi.json' })