Routing

Pearl's Router gives you a clean, readable API for defining HTTP routes. Every handler receives a single ctx argument — your one-stop access point for reading the request and writing the response.

Basic routes

Methods map directly to HTTP verbs. Pass a path string and a handler function.

src/server.ts
import { Router } from '@pearl-framework/pearl'

const router = new Router()

router.get('/posts',        async (ctx) => { /* list posts   */ })
router.post('/posts',       async (ctx) => { /* create post  */ })
router.get('/posts/:id',    async (ctx) => { /* get one post */ })
router.put('/posts/:id',    async (ctx) => { /* replace post */ })
router.patch('/posts/:id',  async (ctx) => { /* update post  */ })
router.delete('/posts/:id', async (ctx) => { /* delete post  */ })

Route parameters

Prefix a path segment with : to capture it as a named parameter. Read it with ctx.request.param('name') — params are always strings, so cast to a number if needed.

···
router.get('/posts/:id', async (ctx) => {
  const id = Number(ctx.request.param('id'))  // always a string — cast if needed
  ctx.response.json({ id })
})

// Multiple params work fine
router.get('/users/:userId/posts/:postId', async (ctx) => {
  const userId = ctx.request.param('userId')
  const postId = ctx.request.param('postId')
  ctx.response.json({ userId, postId })
})

Query strings & request body

Read query string values with ctx.request.query(key, default?). Read the parsed JSON body via the ctx.request.body getter — it's a getter, not a method, so no parentheses.

···
// GET /posts?page=2&limit=10
router.get('/posts', async (ctx) => {
  const page  = ctx.request.query('page',  '1')
  const limit = ctx.request.query('limit', '20')
  ctx.response.json({ page, limit })
})

// POST /posts  { "title": "Hello", "content": "World" }
router.post('/posts', async (ctx) => {
  const { title, content } = ctx.request.body as { title: string; content: string }
  ctx.response.created({ title, content })
})

Route middleware

Pass an array of middleware functions as the third argument to protect a route. They run in order before your handler. The built-in Authenticate() middleware checks for a valid Bearer token and rejects the request with a 401 if it's missing or invalid.

···
import { Authenticate, AuthManager } from '@pearl-framework/pearl'

const auth  = app.container.make(AuthManager)
const guard = [Authenticate(auth)]  // define once, reuse anywhere

// These routes require a valid Bearer token
router.post('/posts',       createPost,  guard)
router.delete('/posts/:id', deletePost,  guard)

// On a guarded route, ctx.get('auth.user') returns the authenticated user
router.get('/me', async (ctx) => {
  ctx.response.json(ctx.get('auth.user'))
}, guard)

Route groups

Group routes under a shared prefix to keep your router organised:

···
router.group('/api/v1', (r) => {
  r.group('/posts', (r) => {
    r.get('/',    listPosts)
    r.post('/',   createPost, guard)
    r.get('/:id', getPost)
  })

  r.group('/users', (r) => {
    r.get('/:id', getUser)
  })
})

Global middleware

Call kernel.useMiddleware() to run middleware on every request. Register your error handler first so it wraps all subsequent handlers.

···
await new HttpKernel()
  .useMiddleware([ErrorHandlerMiddleware, CorsMiddleware, LoggerMiddleware])
  .useRouter(router)
  .listen(3000)

Response helpers

ctx.response is chainable. These cover the common cases:

···
ctx.response.json({ data })                  // 200 JSON
ctx.response.created({ data })               // 201 Created
ctx.response.noContent()                     // 204 No Content
ctx.response.status(400).json({ message: 'Bad input' })  // 400
ctx.response.unauthorized()                  // 401
ctx.response.forbidden()                     // 403
ctx.response.notFound('Not found')           // 404
ctx.response.redirect('/login')              // 302 Redirect
ctx.response.status(418).json({ im: 'a teapot' })  // custom status

HttpContext reference

Property / MethodTypeWhat it gives you
ctx.request.bodyunknownParsed JSON request body (getter)
ctx.request.param(key)stringURL parameter value e.g. :id
ctx.request.query(key, default?)stringQuery string value
ctx.request.header(key)string | undefinedIncoming request header
ctx.request.methodstringHTTP method
ctx.request.urlstringFull request URL
ctx.request.ip()stringClient IP address
ctx.responseResponseBuilder for the outgoing response
ctx.get('auth.user')AuthUser | undefinedAuthenticated user (guarded routes)