Routing
July 11, 2026 · View on GitHub
Read this in English or Português (BR).
A route binds a URL path + HTTP method to a callback. This document covers everything Horse can express in a route definition.
For the API of the request and response objects passed to each callback, see Request & Response.
Basic routes
THorse exposes one method per HTTP verb. Each takes a path string and a callback:
THorse.Get ('/ping', procedure(Req: THorseRequest; Res: THorseResponse) begin Res.Send('pong'); end);
THorse.Post ('/items', procedure(Req: THorseRequest; Res: THorseResponse) begin Res.Send('created'); end);
THorse.Put ('/items/:id', ...);
THorse.Patch ('/items/:id', ...);
THorse.Delete('/items/:id', ...);
THorse.Head ('/items/:id', ...);
Method-routing is exact: THorse.Get only matches GET requests to that path. A request with the wrong method on a known path returns 405 Method Not Allowed. A request with an unknown path returns 404 Not Found.
Path parameters
Use a colon-prefixed segment to capture part of the URL:
THorse.Get('/users/:id',
procedure(Req: THorseRequest; Res: THorseResponse)
begin
Res.Send('User: ' + Req.Params['id']);
end);
GET /users/42→User: 42(andReq.Params['id'] = '42')GET /users/→404GET /users→404
Multiple parameters in a single path work the same way:
THorse.Get('/teams/:teamId/members/:memberId',
procedure(Req: THorseRequest; Res: THorseResponse)
var
T, M: string;
begin
T := Req.Params['teamId'];
M := Req.Params['memberId'];
Res.Send(Format('Team %s, member %s', [T, M]));
end);
Path parameters are always strings. Convert them yourself with StrToInt, TryStrToInt, etc.
Query strings
Query strings are accessed via Req.Query:
// GET /search?name=Horse&category=framework
THorse.Get('/search',
procedure(Req: THorseRequest; Res: THorseResponse)
begin
Res.Send('Looking for ' + Req.Query['name'] + ' in ' + Req.Query['category']);
end);
Missing keys return an empty string. If you need to know whether a key was present, use Req.Query.TryGetValue (returns False when absent).
Route groups
Group related routes under a common prefix:
THorse.Group.Prefix('/api/v1')
.Get ('/users', ListUsers)
.Post ('/users', CreateUser)
.Get ('/users/:id', GetUser)
.Put ('/users/:id', UpdateUser)
.Delete('/users/:id', DeleteUser);
The above is identical to writing THorse.Get('/api/v1/users', ...), etc. Groups can carry their own middleware:
THorse.Group
.Use(JWT(SECRET)) // middleware applies to everything in this group
.Prefix('/api/v1/admin')
.Get ('/stats', GetStats)
.Post('/audit', WriteAudit);
Route-level Middlewares (Local)
You can pass an array of route-specific middlewares (array of THorseCallback) to apply checks only to a single endpoint:
// Static Syntax with array of middlewares
THorse.Get('/admin/dashboard', [AuthMiddleware, LoggerMiddleware],
procedure(Req: THorseRequest; Res: THorseResponse)
begin
Res.Send('Admin Dashboard');
end);
// Fluent Route Syntax with array of middlewares
THorse.Route('/reports')
.Get([AuthMiddleware, LoggerMiddleware], GetReportsHandler)
.Post([AuthMiddleware], PostReportHandler);
Route-level middlewares execute after global middlewares and after any group-level middlewares, but right before the final route callback (handler) executes.
Wildcard middleware
THorse.Use(...) registers middleware that runs on every request, regardless of path:
THorse.Use(MyLogger); // every request is logged
THorse.Use('/api', RequireAuth); // only /api/* requires auth
See Middleware for the full story.
The TMethodType enum
Internally, routes are stored by method. The enum lives in Horse.Commons:
type
TMethodType = (mtAny, mtGet, mtPut, mtPost, mtHead, mtDelete, mtPatch);
mtAny is the wildcard used by middleware (THorse.Use) — it matches any method.
Note:
OPTIONS,TRACE, andCONNECTare not inTMethodType. They route asmtAny(matching wildcard middleware only). TheHorse.CORSmiddleware uses this to interceptOPTIONSfor preflight handling. If you need to discriminate by raw verb in your own middleware, useReq.Method: stringorReq.RawWebRequest.Method.
Pattern matching rules
- Case-sensitive path matching:
/Usersand/usersare different routes. - No trailing slash normalisation:
/usersand/users/are different routes. Decide your project convention and stick to it. - First-registered wins for identical patterns — registering
/users/:idtwice is a duplicate; the second registration emits a runtime error in recent Horse versions (Duplicate route detected: [GET] /users/:id). - Parameter segments cannot have multiple colons —
/users/:id:nameis invalid; use two segments instead (/users/:id/:name).
Sub-resources
There's no built-in mount like Express, but you can express sub-resources with groups:
procedure RegisterUsersRoutes(AGroup: THorseCoreGroup);
begin
AGroup
.Get ('', ListUsers)
.Post ('', CreateUser)
.Get ('/:id', GetUser);
end;
// In main:
RegisterUsersRoutes(THorse.Group.Prefix('/api/v1/users'));
The same RegisterUsersRoutes can be mounted under multiple prefixes (/api/v1/users and /api/v2/users) without duplication — useful for API versioning when v2 only adds new endpoints.
Listing routes (for debugging)
Horse doesn't ship a printRoutes() helper, but you can iterate the router tree directly. The simpler approach is to print each route as you register it:
procedure RegisterAndLog(const Method, Path: string; Cb: THorseCallback);
begin
case Method of
'GET': THorse.Get (Path, Cb);
'POST': THorse.Post (Path, Cb);
// ...
end;
WriteLn(Method, ' ', Path);
end;
For non-trivial apps, keep the route registration centralised in one unit so the layout is obvious from a single file.
Radix Router (Optional - Extreme Performance)
For large-scale applications or high-performance APIs with hundreds of routes, Horse optionally includes a routing engine based on a Prefix Tree (Radix Tree).
Unlike the default linear router which performs path resolution in (scanning routes sequentially), the Radix router resolves URLs in , where is the path string length. This guarantees extreme throughput and constant-time routing performance, regardless of the amount of registered routes in your application.
Activating the Radix Router
Simply invoke the static class procedure THorse.UseRadixRouter at the very beginning of your application setup, before registering any route handlers:
begin
THorse.UseRadixRouter;
THorse.Get('/ping',
procedure(Req: THorseRequest; Res: THorseResponse)
begin
Res.Send('pong');
end);
THorse.Listen(9000);
end.
Advanced Routing (Optional Parameters & Regex)
Starting from this version, Horse natively supports URL path routing restrictions based on Regular Expressions and optional path parameters across all routers (THorseRouterTree and THorseRadixRouter).
1. Optional Parameters
By adding a question mark ? at the end of a path parameter (e.g., :id?), you instruct Horse that this parameter might be missing in the requested URL:
THorse.Get('/users/:id?',
procedure(Req: THorseRequest; Res: THorseResponse)
begin
if Req.Params.Items['id'].IsEmpty then
Res.Send('Listing all users')
else
Res.Send('Retrieving user ' + Req.Params.Items['id']);
end);
- Requesting
GET /users/123-> Match! ReturnsRetrieving user 123(Req.Params.Items['id'] = '123') - Requesting
GET /users-> Match! ReturnsListing all users(Req.Params.Items['id'] = '')
2. Regular Expressions Restrictions (Regex)
You can restrict parameter matching by passing a Regex pattern enclosed in parentheses right after the parameter name:
// This route will only match if the 'id' parameter contains only decimal digits
THorse.Get('/users/:id(\d+)',
procedure(Req: THorseRequest; Res: THorseResponse)
begin
Res.Send('Numeric user: ' + Req.Params.Items['id']);
end);
- Requesting
GET /users/456-> Match! ReturnsNumeric user: 456 - Requesting
GET /users/john-> Does not match the Regex constraint (continues matching other paths or returns404 Not Found).
3. Route Precedence & Resolution
The routing engine resolves route ambiguities by matching routes in order of specificity (from most specific to most generic):
- Exact Static Route: e.g.,
GET /users/new - Regex Parametric Route: e.g.,
GET /users/:id(\d+) - Optional / General Parametric Route: e.g.,
GET /users/:id?
See also
- Request & Response — what you do inside the callback.
- Middleware —
THorse.Use, theNextproc, registration order. - Providers — how the transport layer hands the request to your route callback.