Routing and Type Magic
One of the most powerful features of farrow-http is its type-safe routing system. URL patterns automatically infer parameter types and validate at runtime.
Automatic Inference of Path Parameters
Remember manually parseInt() in Express? farrow-http says: "Not needed!"
typescript
// 🎯 Automatic type inference and validation
app.get('/users/<id:int>').use((req) => {
req.params.id // Type: number (validated as integer)
return Response.json({ userId: req.params.id })
})
app.get('/posts/<slug:string>').use((req) => {
req.params.slug // Type: string
return Response.json({ postSlug: req.params.slug })
})
app.get('/products/<price:float>').use((req) => {
req.params.price // Type: number (float)
return Response.json({ price: req.params.price })
})Supported types:
<id:int>→number(integer)<name:string>→string<price:float>→number(float)<active:boolean>→boolean<userId:id>→string(non-empty identifier)
Optional and Array Parameters
typescript
// Optional parameters (?)
app.get('/articles/<category:string>/<id?:int>').use((req) => {
const { category, id } = req.params
// category: string (required)
// id?: number (optional)
if (id) {
return Response.json({ message: `Article ${id} in category ${category}` })
}
return Response.json({ message: `All articles in category ${category}` })
})
// One or more (+)
app.get('/tags/<tags+:string>').use((req) => {
req.params.tags // Type: string[] (at least one)
return Response.json({ tags: req.params.tags })
})
// Zero or more (*)
app.get('/categories/<cats*:string>').use((req) => {
req.params.cats // Type: string[] | undefined
return Response.json({ categories: req.params.cats || [] })
})Union Types (Enumeration Values)
typescript
// Accept only specified values
app.get('/news/<category:tech|business|sports>').use((req) => {
// req.params.category: 'tech' | 'business' | 'sports'
return Response.json({ category: req.params.category })
})
// Literal types (braces)
app.get('/api/<version:{v1}|{v2}>').use((req) => {
// req.params.version: 'v1' | 'v2'
return Response.json({ version: req.params.version })
})Query Parameters
typescript
// Query parameters auto validation
app.get('/search?<q:string>&<page?:int>&<limit?:int>').use((req) => {
const { q, page = 1, limit = 10 } = req.query
// q: string (required)
// page?: number (optional, default value 1)
// limit?: number (optional, default value 10)
return Response.json({
query: q,
page,
limit,
results: []
})
})
// Mix path parameters and query parameters
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 Methods
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()
})Routing Modularity
typescript
import { Router } from 'farrow-http'
// Create independent router
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: 'User does not exist' })
}
return Response.json(user)
})
// Main application mount routes
const app = Http()
app.route('/api/users').use(userRouter)
// URL mapping:
// GET /api/users → userRouter.get('/')
// GET /api/users/123 → userRouter.get('/<id:int>')Nested Routes
typescript
const app = Http({ basenames: ['/api'] })
// First layer: /api
const v1Router = app.route('/v1') // /api/v1
const v2Router = app.route('/v2') // /api/v2
// Second layer: /api/v1/...
const userRouter = Router()
const postRouter = Router()
v1Router.route('/users').use(userRouter) // /api/v1/users
v1Router.route('/posts').use(postRouter) // /api/v1/posts
// Third layer: /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
})Dynamic Route Matching
typescript
// Wildcard routes (catch all)
app.get('/*').use((req) => {
return Response.json({ path: req.pathname })
})
// Specific prefix
app.route('/api/*').use((req) => {
return Response.json({ apiPath: req.pathname })
})Practical Examples
typescript
const app = Http()
// Resource 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)