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

@sunshj/server

v0.1.0-beta.20

Published

一个轻量级、高性能的 Web 服务器框架

Readme

@sunshj/server

一个轻量级、高性能的 Web 服务器框架,结合了 Express.js、Hono.js 和 h3 的优秀特性。

NPM Version TypeScript Node.js License

✨ 特性

  • 🚀 极致性能: 基于 Node.js 原生 HTTP 模块,零外部依赖
  • 🔷 TypeScript 优先: 完整的类型定义和智能提示,路径参数自动类型推断
  • 🔗 线性链式中间件: 简洁高效的中间件系统,支持早期返回和错误处理
  • 🎯 现代 API: 简洁直观,开发体验优秀
  • 📦 轻量级: 仅 36.12KB (CJS),比 Express 小 5 倍
  • 🔧 灵活路由: 动态参数、通配符、嵌套路由、类型化路由组
  • 智能响应: 自动推断响应类型,按需解析
  • 📤 文件上传: 原生支持 FormData 和文件上传解析
  • 🌊 流式响应: 原生支持 ReadableStream,可轻松实现 SSE
  • 🪝 钩子系统: 灵活的生命周期钩子(beforeRequest、beforeResponse、afterResponse、error、notFound)
  • 🌐 Web Standards: 基于 Web 标准 API,兼容多种运行时(Node.js、Deno、Bun、Cloudflare Workers)

🚀 快速开始

安装

npm install @sunshj/server
# 或者
pnpm add @sunshj/server
# 或者
yarn add @sunshj/server

基础使用

import { bodyParser, createApp } from '@sunshj/server'

const app = createApp()

// 注入 body parser 中间件(必须在访问请求体之前注册)
app.use(bodyParser())

// 基础路由
app.get('/', ctx => {
  return { message: 'Hello World!' }
})

// 动态路由参数
app.get('/users/:id', ctx => {
  const { id } = ctx.params
  return { user: { id, name: `User ${id}` } }
})

// 启动服务器
app.listen(3000, server => {
  console.log('🚀 Server running on http://localhost:3000')
  console.log('Server address:', server.address())
})

📚 核心概念

Context 上下文

每个请求处理函数都会接收一个 Context 对象,包含请求信息和响应方法:

app.get('/api/users/:id', ctx => {
  // 路径参数
  const { id } = ctx.params

  // 查询参数
  const { page = '1', limit = '10' } = ctx.query

  // 请求头 (Web Standard Headers)
  const userAgent = ctx.headers.get('user-agent')

  // 智能响应
  return ctx.status(200).json({
    user: { id },
    pagination: { page: Number(page), limit: Number(limit) },
    userAgent
  })
})

路由定义

支持所有标准 HTTP 方法:

app.get('/users', getAllUsers)
app.post('/users', createUser)
app.put('/users/:id', updateUser)
app.delete('/users/:id', deleteUser)
app.patch('/users/:id', patchUser)

// 链式路由
app.route('/users/:id').get(getUser).put(updateUser).delete(deleteUser)

// 支持多种 HTTP 方法
app.get('/users', getAllUsers)
app.post('/users', createUser)
app.put('/users/:id', updateUser)
app.delete('/users/:id', deleteUser)
app.patch('/users/:id', patchUser)
app.head('/users/:id', checkUser)
app.options('/users', getUserOptions)

// 通配符路由
app.get('/static/*', serveStaticFiles)

// 匹配所有方法
app.all('/api/*', authMiddleware)

中间件系统

框架采用线性链式中间件模型(类似 Express),简洁高效且易于理解。

中间件基础概念

中间件是一个可以访问请求上下文 (Context) 的函数,可以:

  • 在请求到达路由处理器之前执行预处理逻辑
  • 通过调用 await ctx.next() 将控制权传递给下一个处理器
  • 通过返回 Response 提前终止请求处理链

重要特性

  • 中间件和路由处理器使用相同的 RouteHandler 类型
  • 中间件按注册顺序依次执行(线性模型)
  • await ctx.next() 现在是可选的:中间件如果不调用 ctx.next() 且没有返回值,会自动继续执行下一个处理器
  • await ctx.next() 之后的代码不会执行(这是设计特性,非 bug)
  • 如需响应后逻辑(如日志、清理),请使用 afterResponse hook

全局中间件

应用于所有请求的中间件:

// 日志中间件 - ctx.next() 现在是可选的
app.use(ctx => {
  console.log(`→ ${ctx.method} ${ctx.url.pathname}`)
  // ✅ 不调用 ctx.next() 也会自动继续到下一个处理器
})

// 也可以显式调用 ctx.next()
app.use(async ctx => {
  console.log(`→ ${ctx.method} ${ctx.url.pathname}`)
  await ctx.next() // 显式继续到下一个处理器
  // ⚠️ 注意:这之后的代码不会执行(线性链式模型的设计)
  // 如需响应后逻辑,请使用 afterResponse hook
})

// 计时中间件
app.use(ctx => {
  const start = Date.now()
  // 可以省略 ctx.next(),会自动继续
  // ⚠️ 注意:这里无法计算响应时间,应该使用 afterResponse hook
})

// 正确的计时方式:使用 afterResponse hook
app.on('afterResponse', (request, response, ctx, duration) => {
  console.log(`${ctx.method} ${ctx.url.pathname} - ${duration}ms`)
})

路径中间件

只对特定路径生效的中间件:

// API 认证中间件
app.use('/api/*', async ctx => {
  const token = ctx.headers.get('authorization')
  if (!token) {
    return ctx.error(401, 'Missing authorization header')
  }

  // 验证 token
  try {
    const user = await verifyToken(token)
    ctx.user = user // 扩展 context
    // ctx.next() 是可选的,这里会自动继续
  } catch {
    return ctx.error(401, 'Invalid token')
  }
})

// 管理员权限验证
app.use('/admin/*', ctx => {
  if (!ctx.user?.isAdmin) {
    return ctx.error(403, 'Admin access required')
  }
  // 不需要 ctx.next(),会自动继续
})

中间件早期返回

中间件可以通过返回 Response 来终止请求处理链:

// 认证中间件示例
function authMiddleware(ctx) {
  if (!ctx.headers.get('authorization')) {
    return ctx.error(401, 'Unauthorized') // 🚀 直接返回,停止执行链
  }
  // 不需要 ctx.next(),会自动继续到下一个处理器
}

// 使用多个中间件
app.post(
  '/protected',
  authMiddleware, // 第一个中间件:认证
  async ctx => {
    // 第二个中间件:验证请求体
    const data = await ctx.readBody()
    if (!data.name) {
      return ctx.error(400, 'Name is required')
    }
    // ctx.next() 是可选的
  },
  async ctx => {
    // 最终的路由处理器
    const data = await ctx.readBody()
    return { message: 'Success', data }
  }
)

路由级中间件

多个中间件可以作为参数传入路由方法:

// 定义中间件函数 - ctx.next() 是可选的
function logMiddleware(ctx) {
  console.log(`Processing: ${ctx.url.pathname}`)
  // 不需要 ctx.next(),会自动继续
}

function validateMiddleware(ctx) {
  const contentType = ctx.headers.get('content-type')
  if (!contentType?.includes('application/json')) {
    return ctx.error(400, 'Content-Type must be application/json')
  }
  // ctx.next() 是可选的
}

// 应用多个中间件到特定路由
app.get('/protected', authMiddleware, logMiddleware, ctx => {
  return { message: 'Protected route accessed', user: ctx.user }
})

app.post('/data', validateMiddleware, logMiddleware, async ctx => {
  const data = await ctx.readBody()
  return { received: data }
})

中间件执行流程

// 执行顺序示例 - ctx.next() 现在是可选的
app.use(ctx => {
  console.log('1. 全局中间件')
  // 不需要 ctx.next(),会自动继续
})

app.use('/api/*', ctx => {
  console.log('2. 路径中间件')
  // ctx.next() 是可选的
})

app.get(
  '/api/users',
  ctx => {
    console.log('3. 第一个路由中间件')
    // 可以省略 ctx.next()
  },
  ctx => {
    console.log('4. 第二个路由中间件')
    // 自动继续到下一个处理器
  },
  ctx => {
    console.log('5. 最终路由处理器')
    return { message: 'Done' }
  }
)

// 请求 GET /api/users 时的执行顺序:
// 1. 全局中间件
// 2. 路径中间件
// 3. 第一个路由中间件
// 4. 第二个路由中间件
// 5. 最终路由处理器

中间件作用域

框架通过 routerId 实现中间件作用域隔离。由于 App extends Router,App 和 Router 实例都有唯一的 routerId

// 1. App 级全局中间件 (routerId === app.id, 无 path)
// 应用到所有路由,包括挂载的 Router 路由
app.use(ctx => {
  console.log('App middleware')
  // ctx.next() 是可选的
})

// 2. App 级路径中间件 (routerId === app.id, 有 path)
// 应用到匹配路径的所有路由
app.use('/api/*', ctx => {
  console.log('API middleware')
  // 不需要 ctx.next()
})

// 3. Router 级全局中间件 (routerId === apiRouter.id, 无 path)
// 只应用到该 Router 的路由
const apiRouter = createRouter()
apiRouter.use(ctx => {
  console.log('Router middleware - 只对 apiRouter 的路由生效')
  // 自动继续
})
apiRouter.get('/users', ctx => ({ users: [] }))

// 4. Router 级路径中间件 (routerId === apiRouter.id, 有 path)
// 只应用到该 Router 中匹配路径的路由
apiRouter.use('/admin/*', ctx => {
  console.log('Router path middleware - 只对 apiRouter 的 /admin/* 路由生效')
  // ctx.next() 可选
})
apiRouter.get('/admin/users', ctx => ({ adminUsers: [] }))

// 挂载路由器
app.use('/api', apiRouter)

// 请求 GET /api/users 的中间件执行顺序:
// 1. App 全局中间件 (routerId === app.id)
// 2. App 路径中间件 /api/* (routerId === app.id)
// 3. Router 全局中间件 (routerId === apiRouter.id)
// 4. 路由处理器 /users

// 请求 GET /api/admin/users 的中间件执行顺序:
// 1. App 全局中间件 (routerId === app.id)
// 2. App 路径中间件 /api/* (routerId === app.id)
// 3. Router 全局中间件 (routerId === apiRouter.id)
// 4. Router 路径中间件 /admin/* (routerId === apiRouter.id)
// 5. 路由处理器 /admin/users

条件中间件

根据条件动态执行逻辑:

// 认证检查 - ctx.next() 是可选的
function requireAuth(ctx) {
  if (!ctx.headers.get('authorization')) {
    return ctx.error(401, 'Authentication required')
  }
  // 不需要 ctx.next(),会自动继续
}

// 角色检查
function requireRole(role: string) {
  return ctx => {
    if (ctx.user?.role !== role) {
      return ctx.error(403, `Role ${role} required`)
    }
    // ctx.next() 可选
  }
}

// 使用条件中间件
app.get('/user/profile', requireAuth, ctx => {
  return { user: ctx.user }
})

app.get('/admin/settings', requireAuth, requireRole('admin'), ctx => {
  return { settings: {} }
})

中间件最佳实践

// ✅ 推荐:使用 afterResponse hook 处理响应后逻辑
app.on('afterResponse', (request, response, ctx, duration) => {
  // 记录日志
  logger.info({
    method: ctx.method,
    path: ctx.url.pathname,
    status: response.status,
    duration
  })
})

// ❌ 不推荐:在中间件的 ctx.next() 之后写代码(不会执行)
app.use(async ctx => {
  await ctx.next()
  console.log('这段代码不会执行') // ❌
})

// ✅ 推荐:中间件只做请求预处理,ctx.next() 是可选的
app.use(ctx => {
  ctx.startTime = Date.now()
  // 不需要 ctx.next(),会自动继续
})

// ✅ 推荐:明确的早期返回
app.use(ctx => {
  if (condition) {
    return ctx.error(400, 'Bad request') // 明确终止
  }
  // ctx.next() 是可选的,会自动继续执行
})

路由分组

使用路由器创建模块化的路由:

import { createRouter } from '@sunshj/server'

// API 路由
const apiRouter = createRouter()
apiRouter.get('/users', getAllUsers)
apiRouter.post('/users', createUser)
apiRouter.get('/users/:id', getUser)

// 管理员路由
const adminRouter = createRouter()
adminRouter.use(adminAuthMiddleware) // 路由级中间件
adminRouter.get('/stats', getStats)
adminRouter.get('/logs', getLogs)

// 挂载路由器
app.use('/api/v1', apiRouter)
app.use('/admin', adminRouter)

智能响应

框架会自动推断响应类型,让开发更简单:

// 自动 JSON 响应
app.get('/api/user', ctx => {
  return { id: 1, name: 'John' } // → Content-Type: application/json
})

// 自动文本响应
app.get('/health', ctx => {
  return 'OK' // → Content-Type: text/plain
})

// 自动 HTML 响应
app.get('/home', ctx => {
  return '<h1>Welcome!</h1>' // → Content-Type: text/html
})

// HTML 模板字符串语法
app.get('/profile', ctx => {
  const name = 'John'
  return ctx.html`<div><h1>Hello, ${name}!</h1></div>`
})

// 显式响应方法
app.get('/explicit', ctx => {
  return ctx.json({ success: true })
  // 或 ctx.text('Hello')
  // 或 ctx.html('<h1>Hello</h1>')
  // 或 ctx.redirect('/login')
})

// 直接返回 Response 对象(用于高级场景)
app.get('/custom', ctx => {
  return new Response('Custom response', {
    status: 200,
    headers: {
      'Content-Type': 'text/plain',
      'X-Custom-Header': 'value'
    }
  })
})

// 链式调用设置状态码
app.post('/create', ctx => {
  return ctx.status(201).json({ created: true, id: 123 })
})

// 错误响应
app.get('/error', ctx => {
  return ctx.error(404, 'Resource not found')
})

请求体处理

现代化的请求体处理,智能且高效:

import { bodyParser, createApp } from '@sunshj/server'

const app = createApp()

// 1. 注入 body parser 中间件(必须)
app.use(
  bodyParser({
    maxSize: 10 * 1024 * 1024, // 全局最大 10MB
    limits: {
      json: 1 * 1024 * 1024, // JSON 限制 1MB
      urlencoded: 1 * 1024 * 1024, // URL-encoded 限制 1MB
      multipart: 50 * 1024 * 1024, // Multipart 限制 50MB
      text: 1 * 1024 * 1024 // Text 限制 1MB
    },
    strict: false, // JSON 解析失败时降级为文本(默认 false)
    allowEmpty: true // 允许空请求体(默认 true)
  })
)

// 2. 使用 ctx.readBody() 读取请求体(推荐)
app.post('/api/users', async ctx => {
  const userData = await ctx.readBody() // ✅ 推荐:更好的类型推断

  // 数据验证
  if (!userData.name || !userData.email) {
    return ctx.error(400, 'Name and email are required')
  }

  const user = await createUser(userData)
  return ctx.status(201).json(user)
})

// ctx.body 已废弃,但仍可用
app.post('/api/data', async ctx => {
  const data = await ctx.body // ⚠️ 已废弃,建议使用 ctx.readBody()
  return { received: data }
})

// 获取类型化查询参数
app.get('/search', ctx => {
  const query = ctx.getQuery<{ q: string; page?: string }>()
  return { query: query.q, page: Number(query.page || 1) }
})

// 文件上传示例
app.post('/upload', async ctx => {
  // 使用便捷方法获取单个文件
  const file = await ctx.file('avatar') // 获取名为 'avatar' 的文件

  if (!file) {
    return ctx.error(400, 'No file uploaded')
  }

  // 使用 Web Standards File API
  const filename = `${Date.now()}-${file.name}`
  const arrayBuffer = await file.arrayBuffer()

  // 保存文件
  await writeFile(filename, Buffer.from(arrayBuffer))

  return {
    message: 'File uploaded successfully',
    filename,
    size: file.size,
    type: file.type
  }
})

// 多文件上传
app.post('/upload/multiple', async ctx => {
  // 使用便捷方法获取多个文件
  const files = await ctx.files('photos') // 获取名为 'photos' 的所有文件

  if (!files || files.length === 0) {
    return ctx.error(400, 'No files uploaded')
  }

  const uploadedFiles = await Promise.all(
    files.map(async file => {
      const filename = `${Date.now()}-${file.name}`
      const arrayBuffer = await file.arrayBuffer()
      await writeFile(filename, Buffer.from(arrayBuffer))
      return { filename, size: file.size }
    })
  )

  return {
    message: `${files.length} files uploaded successfully`,
    files: uploadedFiles
  }
})

// 获取表单字段
app.post('/form', async ctx => {
  const name = await ctx.field('name') // 获取单个字段
  const tags = await ctx.fields('tags') // 获取字段数组
  const files = await ctx.files('documents') // 获取文件数组

  return { name, tags, fileCount: files.length }
})

// 直接访问完整的 ParsedFormData
app.post('/upload/advanced', async ctx => {
  const body = await ctx.readBody<ParsedFormData>()

  // ParsedFormData 提供辅助方法
  const singleFile = body.file('avatar') // 获取单个文件
  const multipleFiles = body.fileArray('docs') // 获取文件数组
  const fieldValue = body.field('name') // 获取单个字段值
  const fieldArray = body.fieldArray('tags') // 获取字段数组

  return {
    file: singleFile?.name,
    docs: multipleFiles.map(f => f.name),
    name: fieldValue,
    tags: fieldArray
  }
})

Body Parser 特性:

  • 必须注入: 使用 bodyParser() 中间件注入解析器
  • 惰性解析: 只在访问 ctx.readBody()ctx.body 时解析
  • 智能检测: 根据 Content-Type 自动选择解析方式
  • 大小限制: 可针对不同类型设置不同的大小限制
  • 类型安全: 完整的 TypeScript 类型支持
  • Web Standards: 使用原生 request.json(), request.formData() 等 API
  • 便捷方法: ctx.file(), ctx.files(), ctx.field(), ctx.fields() 简化文件和字段访问

🔥 高级用法

Server-Sent Events (SSE)

框架原生支持返回 ReadableStream,可轻松实现 SSE 流式推送:

app.get('/events', () => {
  const stream = new ReadableStream({
    start(controller) {
      let count = 0
      const interval = setInterval(() => {
        if (count < 10) {
          // 发送 SSE 格式的数据
          controller.enqueue(`data: ${JSON.stringify({ count, time: Date.now() })}\n\n`)
          count++
        } else {
          clearInterval(interval)
          controller.close()
        }
      }, 1000)
    },
    cancel() {
      console.log('SSE stream cancelled')
    }
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive'
    }
  })
})

客户端连接:

const eventSource = new EventSource('/events')
eventSource.addEventListener('message', event => {
  const data = JSON.parse(event.data)
  console.log('Received:', data)
})

错误处理

// 全局错误处理中间件
app.use(async ctx => {
  try {
    await ctx.next()
  } catch (error) {
    console.error('Request error:', error)

    if (error.code === 'VALIDATION_ERROR') {
      return ctx.error(400, error.message)
    }

    return ctx.error(500, 'Internal Server Error')
  }
})

// 自定义错误类
class ValidationError extends Error {
  constructor(message: string) {
    super(message)
    this.code = 'VALIDATION_ERROR'
  }
}

app.post('/validate', async ctx => {
  const data = await ctx.readBody()

  if (!data.email) {
    throw new ValidationError('Email is required')
  }

  return { valid: true }
})

Web Standards 兼容性

框架基于 Web 标准 API 构建,提供 fetch 方法兼容多种运行时:

import { createApp } from '@sunshj/server'

const app = createApp()

app.get('/hello', ctx => {
  return { message: 'Hello, World!' }
})

// 使用 Web Standard Request/Response
const request = new Request('http://localhost:3000/hello')
const response = await app.fetch(request)
const data = await response.json()
console.log(data) // { message: 'Hello, World!' }

// 兼容 Cloudflare Workers、Deno、Bun 等运行时
export default {
  fetch: app.fetch
}

Web Standards 特性:

  • ✅ 使用标准 RequestResponse 对象
  • ✅ 支持 fetch(request) 方法调用
  • ✅ 兼容 Node.js、Deno、Bun、Cloudflare Workers
  • ✅ 完全符合 Web API 规范

Hooks 生命周期

框架提供完整的请求生命周期钩子,用于日志、监控和自定义处理:

// beforeRequest - 请求到达时(无法中断请求)
app.on('beforeRequest', (request, ctx) => {
  console.log(`→ ${ctx.method} ${ctx.url.pathname}`)
})

// beforeResponse - 响应发送前(可修改响应)
app.on('beforeResponse', (response, ctx) => {
  const headers = new Headers(response.headers)
  headers.set('X-Powered-By', '@sunshj/server')
  return new Response(response.body, {
    status: response.status,
    headers
  })
})

// afterResponse - 响应发送后(日志、性能统计、清理)
// 这是实现"响应后逻辑"的推荐方式(而不是在中间件的 ctx.next() 之后写代码)
app.on('afterResponse', (request, response, ctx, duration) => {
  // 记录完整的请求/响应日志
  console.log({
    method: ctx.method,
    path: ctx.url.pathname,
    status: response.status,
    duration: `${duration}ms`,
    userAgent: request.headers.get('user-agent')
  })

  // 发送性能指标到监控系统
  // metrics.record('http.request.duration', duration, {
  //   method: ctx.method,
  //   status: response.status
  // })
})

// notFound - 自定义 404 处理
app.on('notFound', ctx => {
  return ctx.status(404).json({
    error: 'Not Found',
    path: ctx.url.pathname,
    message: '请求的资源不存在'
  })
})

// error - 全局错误处理
app.on('error', (error, ctx) => {
  console.error('Error:', error)
  return ctx.status(500).json({
    error: 'Internal Server Error',
    message: error.message
  })
})

Hooks 执行顺序:

1. beforeRequest  → 请求到达
2. 中间件链执行
3. 路由处理器执行
4. notFound       → (如果没有匹配的路由)
5. beforeResponse → 修改响应
6. 发送响应到客户端
7. afterResponse  → 后台日志/清理(不阻塞响应)
8. error          → (如果有错误发生)

特别说明:

  • afterResponse 在响应发送后执行,不会阻塞响应
  • afterResponse 中的所有注册的处理器都会执行(不会早期返回)
  • afterResponse 适合用于日志、监控、资源清理等场景
  • 这是实现"响应后逻辑"的推荐方式,而不是在中间件的 await ctx.next() 之后写代码(那些代码不会执行)
  • 其他 hooks 会在第一个返回值时停止执行

性能监控

使用 afterResponse hook 实现性能监控(推荐方式):

// 使用 afterResponse hook 记录请求时长
app.on('afterResponse', (request, response, ctx, duration) => {
  console.log(`${ctx.method} ${ctx.url.pathname} - ${duration.toFixed(2)}ms`)
})

// 请求计数器和日志
let requestCount = 0
app.on('beforeRequest', (request, ctx) => {
  requestCount++
  console.log(`[${requestCount}] ${ctx.method} ${ctx.url.pathname}`)
})

内容协商

app.get('/api/data', ctx => {
  const accept = ctx.headers.get('accept')
  const data = { message: 'Hello', timestamp: Date.now() }

  if (accept?.includes('application/xml')) {
    return new Response(`<data><message>${data.message}</message></data>`, {
      headers: { 'Content-Type': 'application/xml' }
    })
  }

  return ctx.json(data) // 默认 JSON
})

📖 TypeScript 支持

框架提供完整的 TypeScript 支持和类型推断:

导出的类和函数

// 导入所有可用的类、函数和类型
import {
  App, // 应用程序类
  BodyParser, // 请求体解析器类
  bodyParser, // 创建 body parser 中间件
  Context, // 请求上下文类
  ContextResponse, // 响应方法类
  ContextStore, // 状态管理类
  createApp, // 创建应用实例
  createLogger, // 创建日志器实例
  createRouter, // 创建路由器实例
  createRpcClient, // 创建 RPC 客户端
  defineRouteHandler, // 定义类型化路由处理器
  Logger, // 日志器类
  logger, // 默认日志器实例
  MiddlewareManager, // 中间件管理器基类
  RouteGroup, // 路由组类
  RouteHandlerError, // 路由处理器错误
  Router, // 路由器类
  RpcError, // RPC 调用错误
  withRPC, // 为 App 添加 RPC 功能
  type Hooks, // 生命周期钩子类型
  type HTTPMethod, // HTTP 方法类型
  type LogLevel, // 日志级别类型
  type ParsedFormData, // 解析后的表单数据类型(包含辅助方法)
  type ParsedFormDataResult, // 解析后的表单数据类型(无方法)
  type Route, // 路由配置类型
  type RouteHandler // 路由处理器类型
} from '@sunshj/server'

// 从 body-parser 模块导入错误类(用于错误处理)
import { BodyParseError, BodySizeError } from '@sunshj/server/body-parser'

// 扩展 Context 类型
declare module '@sunshj/server' {
  interface Context {
    user?: { id: string; name: string }
  }
}

// 类型化的处理函数
const getUser: RouteHandler<any, { id: string }> = async ctx => {
  const { id } = ctx.params // id 自动推断为 string
  const user = await findUserById(id)

  if (!user) {
    return ctx.error(404, 'User not found')
  }

  return { user }
}

// 🔷 路由组完整类型支持
// 路径参数类型自动推断
app.route('/users/:id').get(ctx => {
  const { id } = ctx.params // ✅ id 类型为 string
  return { user: { id } }
})

app.route('/posts/:postId').get('/comments/:commentId?', ctx => {
  const { postId, commentId } = ctx.params // ✅ 完整类型推断: { postId: string, commentId?: string }
  return { postId, commentId }
})

// 类型化的中间件(使用 RouteHandler 类型)
// 注意:中间件和路由处理器使用相同的 RouteHandler 类型
const logMiddleware: RouteHandler = ctx => {
  console.log(`${ctx.method} ${ctx.url.pathname}`)
  // ctx.next() 是可选的,不调用也会自动继续到下一个处理器
}

// 泛型支持
interface CreateUserBody {
  name: string
  email: string
}

app.post('/users', async (ctx: Context<CreateUserBody>) => {
  const userData = await ctx.body // 类型为 CreateUserBody
  return { user: userData }
})

类型说明

  • RouteHandler<Body, Params, Query> - 统一的处理器类型,用于中间件和路由处理器
  • 中间件可以调用 await ctx.next() 显式继续执行链,也可以省略(自动继续)
  • 路由处理器必须返回值作为响应
  • 返回值会自动转换为 Response 对象(通过智能响应检测)

⚡ 与 Express.js 对比

| 特性 | @sunshj/server | Express.js | | ---------- | --------------------- | --------------- | | 包大小 | 36.12KB (CJS) | 197KB | | 依赖数量 | 0 | 20+ | | TypeScript | 原生支持 | 需要 @types 包 | | 响应 API | 现代化,自动推断 | 传统 res.json() | | 性能 | 高性能,原生 HTTP | 较重的抽象层 | | 中间件模型 | 线性执行 | 线性执行 |

🔄 迁移示例

Express.js:

const express = require('express')

const app = express()
app.use(express.json())

app.get('/users/:id', (req, res) => {
  res.json({
    user: { id: req.params.id }
  })
})

app.post('/user', (req, res) => {
  const data = req.body
  res.json({
    user: { data }
  })
})

app.listen(3000)

@sunshj/server:

import { bodyParser, createApp } from '@sunshj/server'

const app = createApp()
app.use(bodyParser())

app.get('/users/:id', ctx => {
  return {
    user: { id: ctx.params.id }
  }
})

app.post('/user', async ctx => {
  const data = await ctx.readBody()
  return {
    user: { data }
  }
})

app.listen(3000)

迁移优势:

  • ✅ 代码量减少 30%
  • ✅ 原生 TypeScript 支持
  • ✅ 更现代的 API 设计

📋 API 参考

核心函数

createApp(options?)

创建应用实例。

import { createApp } from '@sunshj/server'

// 基础用法
const app = createApp()

// 带配置选项
const app = createApp({
  server: {
    timeout: 120000, // 请求超时时间(毫秒)
    keepAliveTimeout: 5000, // Keep-Alive 超时时间(毫秒)
    maxHeaderSize: 16384 // 最大请求头大小(字节)
  }
})

createRouter()

创建路由器实例。

import { createRouter } from '@sunshj/server'
const router = createRouter()

defineRouteHandler()

定义类型化的路由处理函数,提供完整的类型推断支持。

import { defineRouteHandler } from '@sunshj/server'

// 定义类型化的处理函数
const getUserHandler = defineRouteHandler<
  any, // Body type
  { id: string }, // Params type
  { include?: string } // Query type
>(async ctx => {
  const { id } = ctx.params // 自动推断为 string
  const { include } = ctx.query // 自动推断为 string | undefined

  const user = await findUserById(id)
  if (!user) {
    return ctx.error(404, 'User not found')
  }

  return { user }
})

app.get('/users/:id', getUserHandler)

// 用于创建可复用的中间件
function zodValidator(schemas: Schema) {
  return defineRouteHandler(async ctx => {
    // 验证逻辑
    await ctx.next()
  })
}

bodyParser(options?)

创建 body parser 中间件,用于解析请求体。

import { bodyParser } from '@sunshj/server'

// 基础用法
app.use(bodyParser())

// 自定义配置
app.use(
  bodyParser({
    maxSize: 5 * 1024 * 1024, // 全局最大 5MB
    limits: {
      json: 1 * 1024 * 1024, // JSON 限制 1MB
      urlencoded: 1 * 1024 * 1024, // URL-encoded 限制 1MB
      multipart: 10 * 1024 * 1024, // Multipart 限制 10MB
      text: 1 * 1024 * 1024 // Text 限制 1MB
    },
    strict: true, // 严格模式:JSON 解析失败时抛出错误
    allowEmpty: true // 允许空请求体
  })
)

App 属性和方法

创建的应用实例提供以下方法:

| 方法 | 类型 | 描述 | | ----------------------------- | ------------------- | -------------------------------- | | app.listen(port, callback?) | void | 启动 HTTP 服务器 | | app.close() | Promise<void> | 关闭 HTTP 服务器 | | app.fetch(request) | Promise<Response> | Web Standards 兼容的请求处理方法 | | app.on(event, handler) | this | 注册生命周期钩子 |

访问底层 HTTP 服务器示例:

const app = createApp()

app.get('/', ctx => ({ message: 'Hello' }))

app.listen(3000, server => {
  // 回调函数接收 server 实例作为参数
  console.log('Server:', server.address())
})

Router 和 MiddlewareManager

Router 类继承自 MiddlewareManager,提供路由注册和中间件管理功能:

// Router 提供的 HTTP 方法
router.get(path, ...handlers)
router.post(path, ...handlers)
router.put(path, ...handlers)
router.delete(path, ...handlers)
router.patch(path, ...handlers)
router.head(path, ...handlers)
router.options(path, ...handlers)
router.all(path, ...handlers) // 匹配所有 HTTP 方法

// 中间件注册
router.use(middleware) // 全局中间件
router.use(path, middleware) // 路径中间件
router.use(path, router) // 挂载子路由器

// 路由组
router.route(basePath) // 返回 RouteGroup 实例

Context 属性

| 属性 | 类型 | 描述 | | ------------- | ------------------------ | ----------------------------------------- | | ctx.params | Record<string, string> | 路径参数 | | ctx.query | Record<string, string> | 查询参数 | | ctx.body | Promise<any> | 请求体 (已废弃,建议使用 ctx.readBody()) | | ctx.headers | Headers | Web Standard Headers 对象 | | ctx.method | HTTPMethod | HTTP 方法 | | ctx.url | URL | URL 对象 | | ctx.next | () => Promise<void> | 调用下一个中间件/处理器 | | ctx.store | Map<string, unknown> | 全局共享状态存储 (来自 ContextStore) |

Context 方法

实例方法

| 方法 | 描述 | 示例 | | ---------------------------- | ------------------------ | ---------------------------------- | | ctx.json(data) | JSON 响应 | ctx.json({ success: true }) | | ctx.text(text) | 文本响应 | ctx.text('Hello World') | | ctx.html(html) | HTML 响应 | ctx.html('<h1>Hello</h1>') | | ctx.html`template` | HTML 模板字符串响应 | ctx.html`<h1>${name}</h1>` | | ctx.status(code) | 设置状态码 | ctx.status(201) | | ctx.redirect(url, status?) | 重定向 | ctx.redirect('/login', 302) | | ctx.error(status, message) | 错误响应 | ctx.error(404, 'Not Found') | | ctx.readBody<T>() | 读取类型化请求体(推荐) | await ctx.readBody<UserData>() | | ctx.getQuery<T>() | 获取类型化查询参数 | ctx.getQuery<{ page: string }>() | | ctx.file(fieldName) | 获取单个上传文件 | await ctx.file('avatar') | | ctx.files(fieldName) | 获取多个上传文件 | await ctx.files('photos') | | ctx.field(fieldName) | 获取单个表单字段 | await ctx.field('name') | | ctx.fields(fieldName) | 获取多个表单字段 | await ctx.fields('tags') | | ctx.provide(key, value) | 提供全局共享状态 | ctx.provide('db', dbInstance) | | ctx.inject<T>(key) | 注入全局共享状态 | ctx.inject<Database>('db') |

静态方法

| 方法 | 描述 | 示例 | | --------------------------------- | ---------------- | ----------------------------------- | | Context.json(data, statusCode?) | 创建 JSON 响应 | Context.json({ ok: true }, 200) | | Context.text(text, statusCode?) | 创建文本响应 | Context.text('Hello', 200) | | Context.html(html, statusCode?) | 创建 HTML 响应 | Context.html('<h1>Hi</h1>', 200) | | Context.error(status, message?) | 创建错误响应 | Context.error(404, 'Not Found') | | Context.provide(key, value) | 提供全局共享状态 | Context.provide('db', dbInstance) | | Context.inject<T>(key) | 注入全局共享状态 | Context.inject<Database>('db') |

错误处理类

框架提供了三个专门的错误类,用于处理不同类型的错误场景:

BodyParseError

请求体解析错误,当解析失败或验证失败时抛出。

import { BodyParseError, BodySizeError } from '@sunshj/server/body-parser'

try {
  const data = await ctx.readBody()
} catch (error) {
  if (error instanceof BodyParseError) {
    console.error('Parse error:', error.message)
    console.error('Raw body:', error.rawBody)
    return ctx.error(400, 'Invalid request body')
  }
}

属性:

  • message: string - 错误信息
  • rawBody: string - 原始请求体内容

BodySizeError

请求体大小超限错误,当请求体或文件超过配置的大小限制时抛出。

try {
  const data = await ctx.readBody()
} catch (error) {
  if (error instanceof BodySizeError) {
    console.error('Size limit exceeded:', error.actualSize, '>', error.limitSize)
    return ctx.error(413, `Request too large: ${error.actualSize} bytes`)
  }
}

属性:

  • message: string - 错误信息
  • actualSize: number - 实际大小(字节)
  • limitSize: number - 限制大小(字节)

RouteHandlerError

路由处理器错误,当路由处理器没有返回值时抛出。

import { RouteHandlerError } from '@sunshj/server'

// 框架内部使用,用户一般不需要手动抛出
// 当最后一个路由处理器没有返回响应时会自动抛出

属性:

  • message: string - 错误信息

日志系统

框架提供内置的日志工具,支持日志级别和格式化输出:

import { createLogger, logger, type LogLevel } from '@sunshj/server'

// 使用默认日志器
logger.info('Server started')
logger.error('Something went wrong')

// 创建自定义日志器
const appLogger = createLogger({
  level: 'debug', // 'debug' | 'info' | 'warn' | 'error' | 'silent'
  prefix: 'MyApp',
  timestamp: true
})

appLogger.debug('Debug message') // 只有 level >= debug 才会输出
appLogger.info('Info message')
appLogger.warn('Warning message')
appLogger.error('Error message')

// 设置日志级别
appLogger.setLevel('warn') // 只输出 warn 和 error
appLogger.setPrefix('NewPrefix')

// 创建子日志器
const childLogger = appLogger.child({ prefix: 'SubModule' })
childLogger.info('Child logger message') // [MyApp:SubModule] ...

Logger 方法:

| 方法 | 描述 | | --------------------- | --------------------------- | | logger.debug(...) | 输出 debug 级别日志 | | logger.info(...) | 输出 info 级别日志 | | logger.warn(...) | 输出 warn 级别日志 | | logger.error(...) | 输出 error 级别日志 | | logger.log(...) | info 的别名 | | logger.setLevel() | 设置日志级别 | | logger.setPrefix() | 设置日志前缀 | | logger.child(opts) | 创建子日志器,继承父级配置 |

RPC 系统

框架提供类型安全的 RPC(远程过程调用)系统,支持服务端和客户端:

// 服务端
import { bodyParser, createApp, withRPC } from '@sunshj/server'

const app = withRPC(createApp())

app.use(bodyParser())

// 定义 RPC 函数
const rpcFunctions = {
  add(a: number, b: number) {
    return a + b
  },
  getUser(id: string) {
    return { id, name: `User ${id}` }
  }
}

// 注册 RPC 处理器(默认路径: /__rpc)
app.registerRpcFunctions(rpcFunctions)

app.listen(3000)
// 客户端
import { createRpcClient, RpcError } from '@sunshj/server/rpc'

// 创建类型安全的 RPC 客户端
const client = createRpcClient<typeof rpcFunctions>('http://localhost:3000', {
  timeout: 30000, // 请求超时(毫秒)
  headers: { Authorization: 'Bearer token' } // 自定义请求头
})

// 调用远程函数(完整类型推断)
const sum = await client.add(1, 2) // 类型: number
const user = await client.getUser('123') // 类型: { id: string, name: string }

// 错误处理
try {
  await client.unknownMethod()
} catch (error) {
  if (error instanceof RpcError) {
    console.error(error.code, error.message)
  }
}

RPC 导出:

| 导出 | 描述 | | ----------------- | --------------------------------------- | | withRPC(app) | 为 App 添加 RPC 功能 | | createRpcClient | 创建类型安全的 RPC 客户端 | | RpcError | RPC 调用错误类 |

🌐 生态系统

虽然框架本身轻量,但可通过中间件无限扩展:

// CORS 中间件
function cors(options = {}) {
  return ctx => {
    if (ctx.method === 'OPTIONS') {
      return new Response(null, {
        status: 204,
        headers: {
          'Access-Control-Allow-Origin': options.origin || '*',
          'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
          'Access-Control-Allow-Headers': 'Content-Type,Authorization'
        }
      })
    }
    // ctx.next() 是可选的,会自动继续
  }
}

// JWT 认证中间件
function jwt(secret: string) {
  return ctx => {
    const token = ctx.headers.get('authorization')?.replace('Bearer ', '')

    if (!token) {
      return ctx.error(401, 'Token required')
    }

    try {
      ctx.user = verifyJWT(token, secret)
      // 不需要 ctx.next(),会自动继续
    } catch {
      return ctx.error(401, 'Invalid token')
    }
  }
}

// 静态文件服务中间件
function serveStatic(dir: string): RouteHandler {
  return async ctx => {
    const filePath = join(dir, ctx.url.pathname)
    try {
      const stats = await stat(filePath)
      if (!stats.isFile()) return await ctx.next()

      const content = await readFile(filePath)
      const ext = filePath.split('.').pop()
      return new Response(content, {
        headers: {
          'Content-Type': getContentType(ext),
          'Content-Length': stats.size.toString()
        }
      })
    } catch {
      return await ctx.next()
    }
  }
}

// 使用中间件
app.use(cors({ origin: 'https://example.com' }))
app.use('/api/*', jwt('your-secret'))
app.use('/*', serveStatic(join(__dirname, 'public')))

📄 许可证

MIT © sunshj