http_router
The http_router library provides the first declarative routing layer
on top of the http_core library. It is implemented as a category so
that router objects can implement the http_handler_protocol protocol
directly while reusing common method dispatch and path-template matching
logic.
Layering
http_router sits between the normalized http_core message layer
and higher-level API authoring helpers:
Use http directly when you only need normalized messages, parsers, generators, or low-level handlers.
Use
http_routerwhen you want the handler object to keep exposinghandle/2while route matching, metadata annotation, middleware, and content negotiation are derived fromroute/4declarations.Add rest when
route/4is still too low-level and you prefer endpoint descriptors plus small normalized result terms.Pair router objects with open_api when you want the route declarations and metadata to derive OpenAPI operations automatically.
Add companion libraries such as http_parameters, http_cors, http_htmx, http_session, http_static_files, and http_directory_listing when you need parameter extraction, middleware helpers, sessions, or static-file routing on the same dispatch layer.
API documentation
Open the ../../apis/library_index.html#http_router link in a web browser.
Loading
To load the library, load the loader.lgt file:
| ?- logtalk_load(http_router(loader)).
Testing
To test this library, load the tester.lgt file:
| ?- logtalk_load(http_router(tester)).
Current scope
The initial routing solution provides one public predicate through the imported category:
handle/2routes a normalized request usingroute/4clauses defined by the importing object.
The remaining router behavior is organized in the sections below.
Route declarations
Importing router objects must define route descriptors using:
route(Id, Method, PathTemplate, Handler)
The Handler argument is the name of a declared local predicate with
arity 2, typically a protected predicate.
Path templates
Path-template atoms support literal segments, anonymous * wildcard
segments, plain {name} placeholders, and typed placeholders such as
{id:integer} and {score:number}. Example:
route(show_user, get, '/users/{id}', show_user).
Request annotations
The router category calls the matched handler after annotating the
request with route(Id) and path_params(Pairs). On the normal
routing path it also scrubs stale internal synthetic properties such as
open_api_probe/1, automatic_options/1, effective_methods/1,
and response_media_type/1 before handler execution.
Route metadata
Importing router objects can optionally declare additional route-specific metadata using:
route_metadata(Id, Metadata)
When defined, Metadata must be a list of compound terms. The router
removes any existing request properties with the same functors, prepends
the metadata to the matched request, and then adds the standard
route/1 and path_params/1 annotations. This keeps route metadata
available to both route handlers and response middleware and allows
metadata descriptors such as summary/1, description/1,
tags/1, or other application-specific terms.
Content negotiation
Importing router objects can optionally declare route response media types using:
route_produces(Id, MediaTypes)
When defined, MediaTypes must be a non-empty list of media type
atoms. The router negotiates the request Accept header against that
list, annotates the matched request with
response_media_type(MediaType), and returns a generic
406 Not Acceptable response when no produced media type matches.
On the normal routing path, any stale incoming response_media_type/1
annotation is scrubbed before route dispatch and replaced only when the
matched route successfully negotiates one of its declared
route_produces/2 media types.
Middleware chaining
Importing router objects can also optionally define ordered middleware descriptors using:
middleware(Id, Handler)
The Handler argument is the name of a declared local predicate with
arity 2 that receives the current request and returns either
continue(Request) or respond(Response). Middleware runs before
route matching, so it can rewrite requests before dispatch or
short-circuit processing with an immediate response.
Response middleware
Importing router objects can also optionally define ordered response middleware descriptors using:
response_middleware(Id, Handler)
The Handler argument is the name of a declared local predicate with
arity 3 that receives the current request, the current response, and
returns the transformed response. Response middleware runs after route
dispatch or short-circuit processing, so it can decorate or rewrite any
generated response.
HEAD fallback
HEAD requests match exact head routes first and otherwise fall
back to a matching get route.
OPTIONS handling
OPTIONS requests match explicit options routes first. When no
explicit options route exists for a matched path, the router returns
an automatic 200 OK response with the derived Allow header and
an empty body. The synthetic request used for this path is annotated
with automatic_options(true) and effective_methods(Methods).
When the router can identify exactly one matching non-options route
template, it also annotates that synthetic request with route(Id),
path_params(Pairs), and that route route_metadata/2 properties
before response middleware runs. When multiple non-options routes
match the same path, the synthetic request omits route/1, keeps
path_params/1 only when all matches produce the same value, and
preserves only metadata properties that are identical across all matched
routes. Automatic OPTIONS can be customized using
route_automatic_options_response/3 and still flows through response
middleware.
Custom bad-request responses
Importing router objects can optionally define:
route_bad_request_response(Request, Errors, Response)
Dedicated route-handler exceptions matching
error(http_parameter_validation(Errors), Context) with a non-empty
Errors list are translated into 400 Bad Request responses before
response middleware runs. The routed request annotations remain
available to the optional route_bad_request_response/3 hook and to
any later response middleware.
Custom error responses
Importing router objects can optionally customize other routing errors by defining:
route_not_found_response(Request, Response)route_method_not_allowed_response(Request, AllowedMethods, Response)
When a path matches but the request method does not, the router returns
a 405 Method Not Allowed response with an Allow header derived
from the matching route descriptors. A get route implicitly
contributes both GET and HEAD to that header. Automatic router
support for OPTIONS also contributes OPTIONS to the header for
matched paths.
The AllowedMethods argument passed to the 405 hook is the
effective method list as lowercase atoms and already includes implicit
head support for get routes and automatic options support.
The Request passed to the 405 hook and to response middleware is
annotated with matched_path(true) and
effective_methods(AllowedMethods).
Custom negotiation failures
Importing router objects can optionally define:
route_not_acceptable_response(Request, ProducedMediaTypes, Response)
The ProducedMediaTypes argument passed to the 406 hook is the
normalized list declared by route_produces/2 for the matched route.
OpenAPI derivation
When a router object also implements the open_api_provider_protocol
protocol, the imported category can automatically derive
operations/1 from route/4, route_metadata/2, and
route_produces/2. Recognized route-metadata terms for OpenAPI
derivation are:
summary(Summary)description(Description)tags(Tags)deprecated(Boolean)security(Requirements)parameters(Parameters)request_body(RequestBody)responses(Responses)
Path-template placeholders automatically derive default OpenAPI path
parameter descriptors using a string schema. Metadata parameters/1
can override those defaults or add extra query, header, or cookie
parameters. When responses/1 is not provided, the router first
attempts to infer a successful response by probing the route handler
with a synthetic annotated request and then falls back to a default
successful response. When available, route_produces/2 guides both
response probing and the derived OpenAPI media types.
When request_body/1 metadata is not provided, the router also
attempts to infer a request body descriptor by probing the route handler
with a synthetic annotated request whose body is left open for the
handler to constrain. The inferred request and response schemas are
emitted inline and are derived from the observed payload terms:
json/1payloads infer JSON Schema types recursively from the observed JSON values.text/1andbinary/1payloads infer a string schema.form/1payloads infer an object schema from the observed key-value pairs.
Explicit request_body/1 or responses/1 metadata always takes
precedence over inferred descriptors.
The imported category also exposes top-level OpenAPI provider
predicates. The default api_info/1 descriptor is derived from the
importing object identifier, servers/1 defaults to [], and
importing objects can override or extend the provider surface with these
hooks:
open_api_info(Info)open_api_servers(Servers)open_api_security(Security)open_api_schema(Name, Schema)open_api_security_scheme(Name, SecurityScheme)
Current limitations
OpenAPI inference is best-effort. Even with the optional
route_open_api_request_body_example/2androute_open_api_response_example/2hooks, explicit metadata remains the right choice for reusable component schemas and for routes whose contracts should be stated directly instead of inferred.