Skip to content

Routing & Handlers

Every endpoint in Moost starts with an HTTP method decorator and a route path. This page covers how to define routes, use parameters, and control which methods your handlers respond to.

HTTP method decorators

Moost provides a decorator for each HTTP method:

DecoratorHTTP Method
@Get(path?)GET
@Post(path?)POST
@Put(path?)PUT
@Delete(path?)DELETE
@Patch(path?)PATCH
@All(path?)All methods
@Upgrade(path?)UPGRADE (WebSocket handshake)

All decorators are imported from @moostjs/event-http.

@Upgrade(path?) (equivalently @HttpMethod('UPGRADE', ...)) registers a WebSocket-upgrade handler. See the WebSocket docs for completing the handshake.

ts
import { Get, Post, Put, Delete, Patch } from '@moostjs/event-http'
import { Controller, Param } from 'moost'

@Controller('users')
export class UserController {
    @Get('')
    list() {
        return [] // GET /users
    }

    @Get(':id')
    find(@Param('id') id: string) {
        return { id } // GET /users/123
    }

    @Post('')
    create() {
        return { created: true } // POST /users
    }

    @Delete(':id')
    remove(@Param('id') id: string) {
        return { deleted: id } // DELETE /users/123
    }
}

Path defaults

The path argument is optional. When omitted, the method name becomes the path:

ts
@Get()
getUsers() { /* GET /getUsers */ }

@Get('')
root() { /* GET / (root of controller) */ }

@Get('list')
getUsers() { /* GET /list */ }

TIP

Use @Get('') (empty string) to handle the controller's root path. Omitting the argument entirely uses the method name as the path segment.

HEAD, OPTIONS, and custom methods

For HTTP methods without a convenience decorator, use @HttpMethod:

ts
import { HttpMethod } from '@moostjs/event-http'

@HttpMethod('HEAD', 'health')
healthCheck() { /* HEAD /health */ }

@HttpMethod('OPTIONS', '')
cors() { /* OPTIONS / */ }

Route parameters

Dynamic segments in a route are prefixed with :. Use @Param('name') to extract them.

ts
@Get('users/:id')
getUser(@Param('id') id: string) {
    return { id }
}

Multiple parameters

Parameters can be separated by slashes or hyphens:

ts
// Slash-separated: /flights/SFO/LAX
@Get('flights/:from/:to')
getFlight(
    @Param('from') from: string,
    @Param('to') to: string,
) { /* ... */ }

// Hyphen-separated: /dates/2024-01-15
@Get('dates/:year-:month-:day')
getDate(
    @Param('year') year: string,
    @Param('month') month: string,
    @Param('day') day: string,
) { /* ... */ }

Regex-constrained parameters

Restrict a parameter's shape with a regex pattern in parentheses:

ts
// Only matches two-digit hours and minutes: /time/09h30m
@Get('time/:hours(\\d{2})h:minutes(\\d{2})m')
getTime(
    @Param('hours') hours: string,
    @Param('minutes') minutes: string,
) { /* ... */ }

Repeated parameters (arrays)

When the same parameter name appears multiple times, the value becomes an array:

ts
// /rgb/255/128/0 → color = ['255', '128', '0']
@Get('rgb/:color/:color/:color')
getRgb(@Param('color') color: string[]) { /* ... */ }

All parameters at once

Use @Params() to get every route parameter as an object:

ts
@Get('asset/:type/:type/:id')
getAsset(@Params() params: { type: string[], id: string }) {
    return params
}

Wildcards

An asterisk (*) matches zero or more characters, including /@Get('*') on a controller prefixed static matches everything under /static/, however deep.

ts
@Controller('static')
export class StaticController {
    // Matches /static/anything/here
    @Get('*')
    handleAll(@Param('*') path: string) { /* ... */ }

    // Matches /static/bundle.js, /static/vendor.js
    @Get('*.js')
    handleJS(@Param('*') path: string) { /* ... */ }

    // Multiple wildcards → array
    @Get('*/test/*')
    handleTest(@Param('*') paths: string[]) { /* ... */ }

    // Regex on wildcard: only digits
    @Get('*(\\d+)')
    handleNumbers(@Param('*') path: string) { /* ... */ }
}

Query parameters

Query strings (?key=value) are not part of the route path. Use @Query to extract them:

ts
import { Get, Query } from '@moostjs/event-http'

@Get('search')
search(
    @Query('q') query: string,
    @Query('page') page: string,
) {
    return { query, page }
}
// GET /search?q=moost&page=2 → { query: 'moost', page: '2' }

Use @Query() without arguments to get all query parameters as an object:

ts
@Get('search')
search(@Query() params: Record<string, string | string[]>) {
    return params
}

INFO

Query values arrive as strings (or string arrays). Use pipes to transform them to numbers or other types.

Gotchas for @Query()

  • Keys ending in [] become arrays: ?tags[]=a&tags[]=b{ 'tags[]': ['a', 'b'] }.
  • Repeating a plain key (?q=a&q=b) triggers an automatic 400 Duplicate key response.
  • When there are no query parameters at all, @Query() resolves to undefined, not {}.

Handler return values

Whatever your handler returns (or resolves to) becomes the response body — strings go out as text/plain, objects as application/json, Node Readable streams are piped. See Responses & Errors for the full return-type table, status codes, headers, and cookies.

Async handlers work the same way — return a promise:

ts
@Get('data')
async getData() {
    const data = await fetchFromDb()
    return data
}

Released under the MIT License.