Skip to main content
Version: 4.0.0-preview

Route Patterns

ExpressoTS v4 ships a small public API: Patterns and pattern(): for constraining route parameters with a regex in a way that survives the Express 5 / path-to-regexp v8 jump.

If you're coming from v3 and used inline regex in routes (@Get("/users/:id(\\d+)")), you'll like this page. The syntax has changed, but the semantics are the same: a path with Patterns.NUMERIC_ID still rejects /users/abc and only dispatches the handler for /users/123.

Why a new API?

Express 5 upgraded path-to-regexp to v8, which dropped inline regex constraints. The literal pattern /users/:id(\\d+) no longer compiles. Frameworks have to opt out of the new matcher and preserve user-facing semantics.

ExpressoTS does this transparently: at decorator time, the HTTP-method decorators parse the constraint out of your path, register the route under a plain :id placeholder, and insert a tiny validator middleware that 404s when the captured value doesn't match the original pattern.

You don't have to think about any of that: just use Patterns / pattern().

The Patterns constants

src/users/users.controller.ts
import { provide } from "@expressots/core";
import {
controller,
Get,
param,
Patterns,
pattern,
} from "@expressots/adapter-express";

@provide(UsersController)
@controller("/users")
export class UsersController {
// Only matches numeric IDs (e.g. /users/123). /users/abc → 404
@Get(`/${pattern("id", Patterns.NUMERIC_ID)}`)
getById(@param("id") id: string) {
return { id: Number(id) };
}

// Only matches UUIDv4 strings
@Get(`/by-uuid/${pattern("uuid", Patterns.UUID)}`)
getByUuid(@param("uuid") uuid: string) {
return { uuid };
}
}

The full set ships out of the box:

ConstantRegexExample match
Patterns.NUMERIC_ID(\\d+)/users/123
Patterns.UUIDUUID v4/docs/550e8400-…
Patterns.SLUG([a-z0-9-]+)/posts/my-post
Patterns.ALPHANUMERIC([a-zA-Z0-9]+)/codes/ABC123
Patterns.LOWERCASE([a-z]+)/tags/javascript
Patterns.UPPERCASE([A-Z]+)/codes/USD
Patterns.EMAILRFC-ish email/users/[email protected]
Patterns.HEXADECIMAL([0-9a-fA-F]+)/colors/ff5733
Patterns.MONGO_ID24 hex chars/users/507f1f77bcf86cd799439011

pattern(paramName, patternValue)

Just sugar that produces :paramName(patternValue):

pattern("id", Patterns.NUMERIC_ID) // → ":id(\\d+)"
pattern("slug", Patterns.SLUG) // → ":slug([a-z0-9-]+)"

You can also write the literal form if you prefer:

@Get("/users/:id(\\d+)") // works, rewritten internally
@Get(`/users/${pattern("id", Patterns.NUMERIC_ID)}`) // recommended: discoverable

Both are equivalent at runtime.

Custom patterns

Anything that compiles as a JavaScript regex is accepted. Build your own:

const Patterns2 = {
SEMVER: "(\\d+\\.\\d+\\.\\d+)",
LOCALE: "([a-z]{2}(?:-[A-Z]{2})?)",
} as const;

@Get(`/releases/${pattern("ver", Patterns2.SEMVER)}`)
getRelease(@param("ver") ver: string) { /* … */ }

@Get(`/i18n/${pattern("locale", Patterns2.LOCALE)}`)
getMessages(@param("locale") locale: string) { /* … */ }

The constraint runs before the controller. If the captured value doesn't match, the framework returns 404 Not Found and the handler is never invoked.

Multiple constraints in one path

You can mix patterns within the same path:

@Get(`/api/${pattern("tenant", Patterns.NUMERIC_ID)}/users/${pattern("id", Patterns.UUID)}`)
getTenantUser(
@param("tenant") tenantId: string,
@param("id") userId: string,
) {
// Matches /api/42/users/550e8400-e29b-41d4-a716-446655440000
// Rejects /api/abc/users/550e8400-… (tenant must be numeric)
// Rejects /api/42/users/not-a-uuid (id must be a UUID)
}

Studio integration

The Studio agent reads the pattern metadata too: your route shows up as GET /api/:tenant/users/:id with the regex constraint annotated so you can verify the contract at a glance.

Combining with validation

Patterns are great for structural filtering, but they don't replace validation. Combine them when you need both shape and semantic checks:

import { z } from "zod";

const UserId = z.string().uuid();

@Get(`/${pattern("id", Patterns.UUID)}`)
getUser(@validatedParam("id", UserId) id: string) {
// Pattern guarantees the URL is well-formed (404 otherwise).
// Zod re-validates at the type level so `id` is `string & UUID`.
}

See also

  • Validation: pluggable schema validation for body/query/params/headers.
  • Decorators: @Get, @param, and the rest of the route surface.
  • API versioning: combine patterns with versioned controllers.