路由与类型魔法
farrow-http 最强大的特性之一就是类型安全的路由系统。URL 模式会自动推导参数类型,并在运行时验证。
路径参数的自动推导
还记得 Express 里要手动 parseInt() 吗? farrow-http 说:"不需要!"
typescript
// 🎯 自动类型推导和验证
app.get('/users/<id:int>').use((req) => {
req.params.id // 类型: number (已验证为整数)
return Response.json({ userId: req.params.id })
})
app.get('/posts/<slug:string>').use((req) => {
req.params.slug // 类型: string
return Response.json({ postSlug: req.params.slug })
})
app.get('/products/<price:float>').use((req) => {
req.params.price // 类型: number (浮点数)
return Response.json({ price: req.params.price })
})支持的类型:
<id:int>→number(整数)<name:string>→string<price:float>→number(浮点数)<active:boolean>→boolean<userId:id>→string(非空标识符)
可选参数和数组参数
typescript
// 可选参数 (?)
app.get('/articles/<category:string>/<id?:int>').use((req) => {
const { category, id } = req.params
// category: string (必需)
// id?: number (可选)
if (id) {
return Response.json({ message: `分类 ${category} 的第 ${id} 篇文章` })
}
return Response.json({ message: `${category} 分类的所有文章` })
})
// 一个或多个 (+)
app.get('/tags/<tags+:string>').use((req) => {
req.params.tags // 类型: string[] (至少一个)
return Response.json({ tags: req.params.tags })
})
// 零个或多个 (*)
app.get('/categories/<cats*:string>').use((req) => {
req.params.cats // 类型: string[] | undefined
return Response.json({ categories: req.params.cats || [] })
})联合类型 (枚举值)
typescript
// 只接受指定的值
app.get('/news/<category:tech|business|sports>').use((req) => {
// req.params.category: 'tech' | 'business' | 'sports'
return Response.json({ category: req.params.category })
})
// 字面量类型 (花括号)
app.get('/api/<version:{v1}|{v2}>').use((req) => {
// req.params.version: 'v1' | 'v2'
return Response.json({ version: req.params.version })
})查询参数
typescript
// 查询参数自动验证
app.get('/search?<q:string>&<page?:int>&<limit?:int>').use((req) => {
const { q, page = 1, limit = 10 } = req.query
// q: string (必需)
// page?: number (可选,默认值 1)
// limit?: number (可选,默认值 10)
return Response.json({
query: q,
page,
limit,
results: []
})
})
// 混合路径参数和查询参数
app.get('/<category:string>?<sort:asc|desc>&<minPrice?:float>').use((req) => {
const { category } = req.params
const { sort, minPrice } = req.query
// category: string
// sort: 'asc' | 'desc'
// minPrice?: number
return Response.json({ category, sort, minPrice })
})HTTP 方法
typescript
// GET
app.get('/users').use(() => {
return Response.json({ users: [] })
})
// POST
app.post('/users').use((req) => {
return Response.status(201).json({ created: true })
})
// PUT
app.put('/users/<id:int>').use((req) => {
return Response.json({ updated: req.params.id })
})
// PATCH
app.patch('/users/<id:int>').use((req) => {
return Response.json({ patched: req.params.id })
})
// DELETE
app.delete('/users/<id:int>').use((req) => {
return Response.status(204).empty()
})
// HEAD
app.head('/users/<id:int>').use((req) => {
return Response.status(200).empty()
})
// OPTIONS
app.options('/users').use(() => {
return Response
.status(200)
.header('Allow', 'GET, POST, PUT, DELETE')
.empty()
})路由模块化
typescript
import { Router } from 'farrow-http'
// 创建独立路由器
const userRouter = Router()
userRouter.get('/').use(() => {
return Response.json({ users: getAllUsers() })
})
userRouter.get('/<id:int>').use((req) => {
const user = getUserById(req.params.id)
if (!user) {
return Response.status(404).json({ error: '用户不存在' })
}
return Response.json(user)
})
// 主应用挂载路由
const app = Http()
app.route('/api/users').use(userRouter)
// URL 映射:
// GET /api/users → userRouter.get('/')
// GET /api/users/123 → userRouter.get('/<id:int>')嵌套路由
typescript
const app = Http({ basenames: ['/api'] })
// 第一层: /api
const v1Router = app.route('/v1') // /api/v1
const v2Router = app.route('/v2') // /api/v2
// 第二层: /api/v1/...
const userRouter = Router()
const postRouter = Router()
v1Router.route('/users').use(userRouter) // /api/v1/users
v1Router.route('/posts').use(postRouter) // /api/v1/posts
// 第三层: /api/v1/users/...
userRouter.get('/').use(() => {
return Response.json({ users: [] }) // GET /api/v1/users
})
userRouter.get('/<id:int>').use((req) => {
return Response.json({ userId: req.params.id }) // GET /api/v1/users/123
})动态路由匹配
typescript
// 通配符路由 (捕获所有)
app.get('/*').use((req) => {
return Response.json({ path: req.pathname })
})
// 特定前缀
app.route('/api/*').use((req) => {
return Response.json({ apiPath: req.pathname })
})实用示例
typescript
const app = Http()
// 资源 CRUD
const bookRouter = Router()
bookRouter.get('/').use(() => {
return Response.json({ books: getBooks() })
})
bookRouter.get('/<id:int>').use((req) => {
const book = getBookById(req.params.id)
if (!book) {
return Response.status(404).json({ error: 'Book not found' })
}
return Response.json(book)
})
bookRouter.post('/').use((req) => {
const book = createBook(req.body)
return Response.status(201).json(book)
})
bookRouter.put('/<id:int>').use((req) => {
const book = updateBook(req.params.id, req.body)
return Response.json(book)
})
bookRouter.delete('/<id:int>').use((req) => {
deleteBook(req.params.id)
return Response.status(204).empty()
})
app.route('/api/books').use(bookRouter)