Catalog · How the catalog works
pascal-api-infra-faa
Infrastructure for REST APIs on Horse, for Delphi and Lazarus/FPC (dual-compiler): paging, ordering, config, file logging, rate limiting, DTO bases, messaging.
Anyone may press Interesting, signed in or not. It helps others find this project.
README
pascal-api-infra-faa
Infrastructure for REST APIs on Horse, for Delphi and
Free Pascal / Lazarus (FPC 3.2.2+, {$MODE DELPHI}), from one source tree.
It ports the ideas of delphi-api-infra-faa (Delphi only) to both compilers, on top of
pascal-common-faa (optional types, clocks,
threading), pascal-jsonmapper-faa
(JSON ⇄ DTO) and pascal-db-faa (database layer,
paging). It is not a drop-in replacement for the Delphi library: DTOs map published
properties, because FPC 3.2.2's RTTI sees nothing else.
Version 0.32.1 (see the changelog): the core, the Horse middlewares, OpenAPI, an MCP
server (protocol 2026-07-28), W3C trace context, metrics (Prometheus), health endpoints,
spans exported to an OpenTelemetry collector (OTLP), opt-in multi-tenancy
(docs/multitenancy-design.md), JWT signing keys that rotate
by themselves, per tenant, sealed in their store (guide), and refresh
tokens used once, a copy that comes back ending its login (guide),
ETag/If-Match against lost updates and Idempotency-Key for retried POSTs
(guide), the tenant from a public path, signed links, and a
package without pascal-db-faa for a client program. The tenant catalog's cache is
pascal-common-faa's IPcCache, in memory or shared by every instance through Redis.
Observability plan: docs/observability-design.md. See docs/plan.md.
Guides
How to build an API with the library, one topic at a time (configuration, errors, DTOs, authentication, database, observability, OpenAPI, MCP, multi-tenancy...): docs/guide, which lists every guide and when to read it. Start with getting started; to keep your own code on both Delphi and Lazarus/FPC, read your project on both compilers.
The guides say how; the design documents (docs/*-design.md) say why each decision was taken
and what was measured.
Contents
| Unit | What it gives you |
|---|---|
PascalApi.Config |
TAppConfig: settings from the environment, then a .env file, then a default |
PascalApi.OrderBy |
TOrderBySpec: the client's "name,-state" to an ORDER BY fragment, through an allow-list |
PascalApi.Pagination |
query-string page/limit to TPageRequest (pascal-db-faa), and the paged JSON envelope |
PascalApi.RateLimitState |
the sliding window behind rate limiting, on a monotonic clock |
PascalApi.FileLog |
asynchronous file logging, one file per category, rotated by size; TLogTruncate |
PascalApi.Dto |
marker interfaces and base classes for DTOs, including paged search and paged response |
PascalApi.Text |
UTF-8 bytes ⇄ string, MD5, UTF-8-safe prefix — the same on both compilers |
PascalApi.HttpTypes |
THttpHeader, with no dependency, so PascalApi.Otlp builds without pascal-db-faa; PascalApi.Http keeps the same type |
PascalApi.HttpTransport |
IHttpTransport: one HTTP exchange, each compiler's own client behind it (THTTPClient on Delphi, fphttpclient with TLS on FPC), every status an answer, EHttpTransport for no answer; the OTLP exporters and the MCP executor send through it. PascalApi.HttpTransport.Fake: a queue of answers and a record of requests, for tests |
PascalApi.SignedLinks |
TSignedLinks: a URL that grants one thing until an expiry (HMAC-SHA256, a key per scope derived from rotating keys, your application's context, constant-time and canonical Base64url checks); needs only pascal-common-faa |
PascalApi.Http |
HTTP exceptions (EValidationException, ENotFoundException...), exception → status mapping, CORS, Bearer, client IP, trace context, access log line (text or JSON), metric labels, health answer; replaceable client messages (English or Portuguese) |
PascalApi.Crypto |
SHA-256, HMAC-SHA256, Base64url (FPC 3.2.2 has no SHA-256): wrappers of pascal-common-faa's PascalCommon.Crypto |
PascalApi.Jwt |
HS256 JSON Web Tokens: sign, verify (alg, signature, exp, nbf), claims; a key id (kid) in the header |
PascalApi.JwtKeys |
signing keys named by kid (guide): a static ring (JWT_KEYS), a ring that generates and rotates its keys in a store every instance shares (TDbJwtKeyStore; Redis in src/redis), secrets sealed with AES-256-GCM under a master key (TSealedJwtKeyStore) |
PascalApi.RefreshTokens |
refresh tokens used once (guide): <scope>.<secret>, only the SHA-256 stored, rotation in a family, a spent token that comes back or two refreshes at once revoking the family, your user check, logout and RevokeUser; stores in a database (TDbRefreshTokenStore, your named SQL) or in memory |
PascalApi.Preconditions |
optimistic concurrency (guide): the row's version as a strong ETag, If-Match read as RFC 9110 says (a list, strong comparison), 428/400/412 with their codes |
PascalApi.Idempotency |
Idempotency-Key (guide): a key per scope and request (SHA-256 fingerprint), claimed with a lease, the answer stored and replayed, a client error replayed and anything else released; stores in a database (TDbIdempotencyStore, your named SQL) or in memory |
PascalApi.Horse.Idempotency (src/horse) |
THorseIdempotentRequest: the key read, the replay sent (Idempotent-Replayed: true), the answer stored, in a handler |
PascalApi.Redis.JwtKeys (src/redis) |
TRedisJwtKeyStore: the keys in Redis, on pascal-redis-faa (outside the package) |
PascalApi.Horse.Middlewares (src/horse) |
Horse middlewares: error handler, CORS, request log (with your own fields: AddField), Bearer auth, JWT, rate limit (key and limit per request: KeyExtractor, LimitFor), Connection: close on FPC 3.2 (TConnectionCloseMiddleware) |
PascalApi.OpenApi |
OpenAPI 3.0.3 document from routes and DTO types; TApiSchema.Describe for metadata |
PascalApi.Horse.OpenApi (src/horse) |
TRouteDoc: register and document a route in one call; /swagger UI and /swagger/doc.json, with the JWT/Bearer scheme taken from the auth middleware |
PascalApi.Mcp |
MCP (2026-07-28): the documented routes as tools, and the JSON-RPC dispatcher |
PascalApi.Horse.Mcp (src/horse) |
TMcpEndpoint.Register('/mcp', ...): the MCP endpoint; tool calls go through the API's own middlewares |
PascalApi.Otlp |
OTLP/HTTP JSON for traces and TOtlpHttpExporter (an IPcSpanExporter for pascal-common-faa's PascalCommon.Tracing), configured by the standard OTEL_* variables; needs neither pascal-db-faa nor Horse (a desktop client can export) |
PascalApi.Horse.Observability (src/horse) |
TMetricsMiddleware (OpenTelemetry HTTP server metrics), TMetricsEndpoint (/metrics, Prometheus text), THealthEndpoint (/health/live, /health/ready with your checks) |
PascalApi.Tenancy |
multi-tenancy, opt-in (guide): the tenant catalog (configuration, or a control database with a TTL cache on pascal-common-faa's IPcCache: its own in memory, or the application's, e.g. in Redis through pascal-redis-faa's TRedisCacheStore, where an Invalidate reaches every instance), ITenantDataResolver (a database per tenant or a shared one, pools named by mode, idle databases dropped), TTenantFilter.Prepare (the TENANT_ID condition and its parameter in one call), the 401/403 refusals, TTenantJwtKeyRing (a signing key ring per tenant), ValidTenantId and the tenant from a path prefix (one 404 for an unknown, suspended or impossible name). Design: docs/multitenancy-design.md |
PascalApi.Horse.Tenancy (src/horse) |
TTenantMiddleware: the tenant from the token's tenant_id claim, checked against the catalog, after TJwtMiddleware; TPathTenantMiddleware: the tenant from /public/<tenant>/... on routes without a token; TTenantMiddleware.Current(AReq) in a handler; TenantIdOf for tenant.id in the access log and spans |
PascalApi.Version |
PASCALAPI_VERSION, for compile-time checks |
A quick look
TErrorHandlerMiddleware.Register(LOnError); // {"error": ...} + status for any exception
THorse.Use(TLoggerMiddleware.New); // one access line per request; X-Request-Id = W3C trace id
THorse.Use(TMetricsMiddleware.New); // http.server.request.duration, .active_requests
THorse.Use(TCorsMiddleware.New('https://app.example.com'));
THorse.Use(TJwtMiddleware.New(TAppConfig.Get('JWT_SECRET'), ['/health', '/metrics', '/auth/login']));
THorse.Use('/reports', TRateLimitMiddleware.New(60, 60));
THorse.Get('/cities', GetCities); // raise ENotFoundException / EValidationException freely;
// answer with TJsonSend.Send(Res, Json) (UTF-8 on both compilers)
TMetricsEndpoint.Register('/metrics'); // Prometheus scrape
THealthEndpoint.AddCheck('database', LChecks.Database); // a function: Boolean (method on FPC)
THealthEndpoint.Register('/health'); // /health/live, /health/ready (200 or 503)
TPcTracing.Start(TPcTracingOptions.FromEnvironment('cities-api', '1.0.0', ReadAppConfig),
TOtlpHttpExporter.FromEnvironment); // spans to an OpenTelemetry collector
THorse.Listen(9000);Documented routes, Swagger UI and MCP tools come from the same declarations:
TRouteDoc.Get('/cities').Summary('List cities').Tag('cities')
.QueryParams<ICityFind>.ResponsePaged<ICity>(200).Register(GetCities);
TRouteDoc.Serve('/swagger', 'Cities API', '1.0.0');
TMcpEndpoint.Register('/mcp', 'http://127.0.0.1:9000', 'cities-api', '1.0.0'); // after every routeAn MCP client (protocol 2026-07-28) then lists list_citie with a JSON Schema of its arguments
(state, page, limit, orderBy...) and calls it: the call is a GET /cities?state=SC to
the API itself, with the client's token, through every middleware.
On FPC a Horse callback is a plain procedure, so each middleware keeps its settings in the unit: one configuration per process.
Messaging, Redis and IPC
Not in this library: use the sibling libraries, same author and dual-compiler rules, sharing pascal-common-faa with this one. They are the first choice for an API built on pascal-api-infra-faa.
| Need | Library | Units |
|---|---|---|
| Message broker (RabbitMQ / AMQP 0-9-1), or a broker embedded in the program | pascal-amqp-faa | AMQP.* |
| Redis: cache, locks, counters, Pub/Sub, Streams | pascal-redis-faa | Redis.* |
| Inter-process: Named Pipe / Unix socket, TCP, TLS | pascal-pipes-faa | Pipes.* |
How to add them to an application, minimum versions and short examples: docs/related-libraries.md; using them inside an API (one client per process, spans, a server that is down): docs/guide/messaging.md.
Samples
| Sample | Shows | Checked by |
|---|---|---|
| 01-api | every middleware (errors, CORS, log, metrics, JWT, rate limit), health, paging and validation, refresh tokens, in memory | tools/http_scenarios.sh (260 checks) |
| 02-db | SQLite through pascal-db-faa (SQLdb on FPC, FireDAC on Delphi): migrations, paging and ordering in SQL, filters, 409 from a unique key, NULL as null |
tools/http_scenarios_db.sh (95 checks, including a PATCH with absent/null/value; also the pool metrics pascal-db-faa records) |
| 03-tenants | multi-tenant with PascalApi.Tenancy: the tenant from a JWT claim, a database per tenant and a shared one with TENANT_ID, 401/403 for a removed or suspended tenant; JWT signing keys static (JWT_KEYS) or generated per tenant, sealed, in the control database or Redis (JWT_KEY_STORE); refresh tokens per tenant in the control database; the catalog cached in memory or in Redis (CATALOG_CACHE) |
tools/http_scenarios_tenants.sh (183 checks, the public route by path among them; most of them one tenant trying another's rows, also through a JOIN; also rate limit by plan, tenant.id in the log, the static keys, refresh tokens under another tenant's prefix); tools/http_scenarios_jwt_rotation.sh (two instances on one key store, rotating every 3 s, and a refresh token issued by one and rotated by the other: 36 checks on the database, 30 on Redis) |
| 04-messaging | Redis (a shared TRedisClient, cache-aside, a fallback when Redis is down) and RabbitMQ (JSON orders published with and without confirms, a consumer that rejects what it can't read) inside an API, with spans; PipesJsonSample: JSON DTOs between processes over pascal-pipes-faa. FPC only, built by hand (build_fpc.sh) against your clones of the sibling libraries |
not by CI: measured by hand for docs/guide/messaging.md and docs/guide/dtos-json.md |
Samples 01-03 run with one source on Delphi and Lazarus/FPC. On FPC for Windows, samples 02 and 03 need
sqlite.org's sqlite3.dll next to the executable (see tools/test_http.sh).
Using it
Add the library's src and its dependencies to the project, each from your single copy
(git submodules of your project), never from this repository's external/:
- Delphi: search path with
src,pascal-common-faa/src,pascal-common-faa/bridges/jsonmapper,pascal-jsonmapper-faa/src,pascal-db-faa/src; for the middlewares alsosrc/horseand Horse'ssrc. - Lazarus: require
packages/pascal_api_infra_faa.lpk(it requirespascal_common_faa,pascaljsonmapper_pkgandpascal_db_faa); for DTO JSON, alsopascal_common_faa_jsonmapper. The middlewares are not in the package (Horse has none): addsrc/horseand Horse'ssrcto the project's search path; for the Redis key store,src/redisand your pascal-redis-faa. pascal-common-faa 1.29.0 or later (the package compilesPascalApi.Tenancy, whose catalog cache isPascalCommon.Cache; on Delphi, withoutPascalApi.Tenancy, 1.26.0 is enough). Use Horse 3.3.3 or later: 3.3.2 doesn't compile on FPC 3.2.2 for Windows (fixed upstream in 3.3.3). Tested with 3.3.12. - A program that talks to an API and has no database (a desktop client): on Lazarus require
packages/pascal_api_infra_faa_client.lpkinstead (0.31.0; it requirespascal_common_faaandpascaljsonmapper_pkgonly):PascalApi.Dto,OpenApi,OrderBy,Text,Config,HttpTypes,HttpTransport(and its fake),Otlp,Version. It is a subset ofpascal_api_infra_faa: a project requires one of the two, never both. On Delphi, the search path withoutpascal-db-faa/src.tests/Deps/ClientNoDbis the check.
Console and service programs on FPC call SetMultiByteConversionCodePage(CP_UTF8) at startup
(LCL programs already run in UTF-8); on Unix, put cthreads and cwstring first in the
program's uses.
Tests
git submodule update --init
sh tools/test_fpc.sh # FPC on Windows (lazbuild)
sh tools/test_fpc_docker.sh # FPC 3.2.2 on Linux, in Docker
sh tools/test_http.sh # the samples over HTTP (FPC on Windows)
sh tools/test_http_docker.sh # the same on Linux, in Docker
sh tools/ci-test.sh # what CI runsOn Delphi, open PascalApi.groupproj and run PascalApi.UnitTests (Win32 and Win64; "Build All"
builds only the active platform). Every suite must end with 0 leaks.
License
MIT. See LICENSE.

