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.
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.
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 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).
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);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 authSee Middleware for the full story.
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.
- 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).
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.
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.
- 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.