Skip to content

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)

This is a third-party Farrow documentation site | Built with ❤️ and TypeScript