From magazine topic to project implementation
Relevant service and technical pages for this post
Why a „REST API with RemObjects SDK“ often gets decided at the margins in practice
A REST API with RemObjects SDK rarely succeeds or fails at the „Hello World“ service; it does so where operations, legacy and integration collide: versioning without downtime, consistent error behavior across all endpoints, reproducible debugging through proxy chains and the ability to unambiguously correlate requests when problems occur.
RemObjects SDK brings a lot of infrastructure: services, message formats, serialization, hosting (e.g. as Windows- and Linux-services or behind IIS/reverse proxy) and defined extension points to handle errors centrally. What is often missing in mature business-software landscapes, however, is a consistently applied contract: which JSON fields are stable? How do we signal errors? How do we recognize a request after it has passed load balancers, TLS termination and multiple backend layers?
The following approach (including Delphi-snippets) shows a robust pattern for RemObjects SDK: version JSON contracts, enforce Correlation-ID (request ID for traceability), translate exceptions into HTTP status and JSON error objects, and do so without pitting debugging and operations against each other. In addition, we look at edge cases that regularly occur in real environments: server threading, database access with BDE replacement with native connectivity, proxy headers, timeouts and „dirty“ client payloads.
Architectural decision: versioning via media type instead of URL
Many APIs version via paths such as /v1/. This is pragmatic, but in long-running integrations (e.g. ERP/DMS/CRM connections) it often leads to URL duplication, doubled routes, doubled tests and the question „which version are we actually using?“ in runbooks.
An alternative is versioning via the media type (content negotiation). The client sends e.g. Accept: application/vnd.company.order+json;v=2. The server deterministically reads the version and adapts contract/DTO behavior accordingly. This works across proxy and cache chains if headers are forwarded intact. For administrators it is also easy to verify: a request can be reproduced via curl/Postman without differing URLs.
RemObjects SDK is not „REST-purist“, but a pragmatic service framework. That is precisely why the media-type variant pays off: you can keep stable endpoints and still evolve contracts. The important part is that you always evaluate the version, decide centrally in one place, and propagate the result into your service context.
When does the Accept-header approach break down?
In practice there are three typical failure points you should address in advance:
- Proxy policies: Some reverse proxies/WAF rules normalize or filter Accept headers. Then your API will silently fall back to the default. Solution: explicitly verify proxy rules and, if necessary, fall back to
X-Api-Version. - Client libraries: Some HTTP clients set their own Accept header and overwrite values. Solution: also support the contract version as an optional query parameter (only as a fallback), or parse the Accept header tolerantly on the server side.
Accept (Vary: Accept), otherwise it may deliver version 1 to version-2 clients. Solution: explicitly set Vary, or disable caching at the API level.Source snippet: Request context, Correlation-ID, Version and Error-Mapping
The code is intentionally structured so it can be integrated into existing RemObjects server projects: a small context layer, a parser for the API version (from Accept), a Correlation-ID mechanism and a central exception mapping. Terms:
- Correlation-ID: Unique ID per request that appears in the response and is referenced in logs.
- Exception-Mapping: Translation of internal Delphi exceptions into stable, client-processable error objects (including HTTP status).
- Contract-Version: Version of the JSON contract that governs behavior and fields.
unit Api.Infrastructure;
interface
uses
System.SysUtils, System.Classes, System.StrUtils, System.Generics.Collections,
System.JSON;
type
EApiError = class(Exception)
private
FHttpStatus: Integer;
FCode: string;
FCorrelationId: string;
public
constructor Create(const AHttpStatus: Integer; const ACode, AMessage, ACorrelationId: string);
property HttpStatus: Integer read FHttpStatus;
property Code: string read FCode;
property CorrelationId: string read FCorrelationId;
end;
TApiContext = record
CorrelationId: string;
ContractVersion: Integer;
RemoteIp: string;
UserAgent: string;
class function New: TApiContext; static;
end;
TApiVersion = record
class function FromAcceptHeader(const AAccept: string; const ADefault: Integer = 1): Integer; static;
end;
TApiErrorMapper = class
public
class function ToErrorJson(const E: Exception; const ACorrId: string): TJSONObject; static;
class function ToHttpStatus(const E: Exception): Integer; static;
class function SafeMessage(const E: Exception): string; static;
end;
implementation
{ EApiError }
constructor EApiError.Create(const AHttpStatus: Integer; const ACode, AMessage, ACorrelationId: string);
begin
inherited Create(AMessage);
FHttpStatus := AHttpStatus;
FCode := ACode;
FCorrelationId := ACorrelationId;
end;
{ TApiContext }
class function TApiContext.New: TApiContext;
begin
Result.CorrelationId := '';
Result.ContractVersion := 1;
Result.RemoteIp := '';
Result.UserAgent := '';
end;
{ TApiVersion }
class function TApiVersion.FromAcceptHeader(const AAccept: string; const ADefault: Integer): Integer;
// Expects e.g.: application/vnd.company.order+json;v=2
var
Parts: TArray<string>;
P: string;
V: string;
I: Integer;
begin
Result := ADefault;
if AAccept.Trim.IsEmpty then
Exit;
Parts := AAccept.Split([';', ',']);
for P in Parts do
begin
V := Trim(P);
if StartsText('v=', V) then
begin
if TryStrToInt(Copy(V, 3, MaxInt), I) and (I > 0) and (I < 100) then
Exit(I);
end;
end;
end;
{ TApiErrorMapper }
class function TApiErrorMapper.SafeMessage(const E: Exception): string;
// In production, no internal details, no SQL, no paths.
// For debug/stage you can extend this via configuration.
begin
if E is EApiError then
Exit(E.Message);
if E is EArgumentException then
Exit('Invalid parameters.');
Exit('Internal error.');
end;
class function TApiErrorMapper.ToHttpStatus(const E: Exception): Integer;
begin
if E is EApiError then
Exit(EApiError(E).HttpStatus);
if E is EArgumentException then
Exit(400);
Exit(500);
end;
class function TApiErrorMapper.ToErrorJson(const E: Exception; const ACorrId: string): TJSONObject;
var
Code: string;
Status: Integer;
Msg: string;
begin
Status := ToHttpStatus(E);
Msg := SafeMessage(E);
if E is EApiError then
Code := EApiError(E).Code
else if E is EArgumentException then
Code := 'bad_request'
else
Code := 'internal_error';
Result := TJSONObject.Create;
Result.AddPair('error', TJSONObject.Create
.AddPair('code', Code)
.AddPair('message', Msg)
.AddPair('httpStatus', TJSONNumber.Create(Status))
.AddPair('correlationId', ACorrId));
end;
end.Purpose: Stable Request Context instead of „somewhere in the thread-local“
The snippet deliberately separates: TApiContext is the minimal state you want to pass through. In RemObjects SDK a lot runs via server-/channel context. In heterogeneous projects (e.g. additional worker threads, DB queue, background jobs) explicit passing is often more robust than implicit thread-locals, because it makes concurrency and context switches more visible.
Prerequisites: The Accept-header variant requires that your reverse proxy (nginx, IIS ARR, Traefik) forwards the header unchanged. In some environments „unusual“ Accept headers are filtered or normalized.
Pitfalls: Versioning via Accept is only as good as your tests. If clients use libraries that overwrite Accept, an API can suddenly fall back to the default. For legacy clients a default fallback is sensible, but it must be visible in monitoring (e.g. a log warning „Version defaulted“).
Alternatives: If you prefer to do versioning via X-Api-Version: the parser is identical, only the source is a different header. From the perspective of gateways that is sometimes easier to control.
Integration into RemObjects SDK: Correlation-ID and Exception Mapping at the Service Entry
The real effect arises when you apply the mechanism consistently at the edge of your server: read from headers once at request entry, translate at exception exit into a stable response. Depending on hosting (e.g. RO-HTTP-Server, IIS hosting, self-operated Windows-/Windows- and Linux-Services) the concrete hook points differ; the principle remains the same: build context, call business logic, map exceptions centrally.
In RemObjects projects it is common to work directly per service method. That scales well initially, but fails in operation: each method implements logging and error handling differently. A clean separation is a service base or a dispatcher that standardizes.
Practical procedure (deliberately concise and implementation-oriented)
- Read Correlation-ID from request header
X-Correlation-ID; if missing, generate server-side (e.g. GUID). - Read contract version from
Accept(or fromX-Api-Version). - Log request start: method, path, Correlation-ID, remote IP, start timing.
- Execute business logic; encapsulate DB accesses in transactions where possible.
- Catch exceptions: determine HTTP status, produce JSON error object, set response header
X-Correlation-ID. - Log request end: status, duration, any error code.
Threading on the server: Why Correlation-ID becomes useless without context discipline
A common Delphi edge case: the service method triggers asynchronous work (e.g. report generation, import, push to a DMS). Then the original request thread is no longer the one that later writes log lines. If the Correlation-ID is only known „at the beginning“, traceability collapses.
Pragmatic rule: Anything that does not remain strictly on the request thread gets the context passed explicitly. Even if that looks like longer parameter lists, it pays off. Alternatively you can work with a clearly defined context object that is deliberately passed to workers (instead of global variables or hidden singletons).
Typical tipping points in RemObjects-/Delphi-servers:
- DB connections per thread: BDE-Ablosung mit nativer Anbindung connections are not automatically safely shareable between threads. A connection pool or one connection per thread is often more sensible than “a global connection”.
- Transaction boundaries: If you have several steps within a request that belong together, the transaction must remain within the same logical unit. Asynchronous work must not accidentally continue inside the same transaction.
- Cancellation: When the client aborts (proxy timeout, browser closed), the server often continues running. Deliberately consider whether background work still makes sense in that case.
Data access and error codes: 409 is not „also a 500“
In integration projects, clean error mapping is more than cosmetics. It determines whether the counterpart (ERP connector, ETL job, customer portal) can react correctly. A few practical guardrails that have proven themselves in Delphi/RemObjects environments:
- 400 Bad Request: Validation, missing/invalid parameters, JSON not parseable. Important: the response should remain stable even if the body is corrupt.
- 401/403: Separate authentication from authorization. 401 means „no/invalid identity“, 403 „identity OK, but forbidden“.
- 404: Resource does not exist. Be cautious with security: do not always reveal whether something exists.
- 409 Conflict: Business/domain conflict (e.g., version conflict, „status does not allow this action“, unique key violation when it is domain-relevant).
- 422 Unprocessable Content: When syntactically everything is OK but domain validation fails (not every team uses 422, but it is often clearer than 400).
- 500: Anything you cannot classify cleanly. This also includes „DB down“, „timeout“, „Unhandled Exception“.
Delphi-specific tip: Many DB errors surface as generic exceptions. It is worthwhile to check for known situations at the data access layer and map them into EApiError. Important: do not include SQL fragments or internal table/column names in the client message. Those details belong in the log, not in the response.
Debugging tip: reproducible errors via a „Contract Snapshot“
Unusual, but extremely helpful in operation: when errors occur (or selectively for certain Correlation-IDs), store a „snapshot“ of request headers + request body in a debug spool file. This is not continuous logging (data protection/volume), but a controlled tool to reproduce hard-to-reproduce cases close to production.
Important: A snapshot must never persist auth headers, tokens or personal data unfiltered. In practice this means: redaction (masking) and activation only via a feature flag or whitelist (e.g., only for certain Correlation-IDs, short time windows).
Clean implementation in practice: mask rather than omit
In real integrations the „critical“ fields are often precisely the ones needed for debugging (e.g., identifiers). Instead of blanket omission, masking is better: partially replace tokens, keep only the domain of an email address, keep only the last digits of an IBAN. This keeps the case reproducible without spreading unnecessary data on the filesystem. Additionally, the snapshot should be clearly marked as a debug artifact and have a defined retention period.
Security and Operations: Header Forwarding, Proxy Chains, and Timeouts
A REST API rarely terminates directly at the client. Typical setups include chains of reverse proxy, TLS termination, WAF, or API gateway. This leads to practical considerations:
- Remote IP: Do not rely blindly on
X-Forwarded-For. Only accept it from trusted proxies; otherwise use the direct socket IP. Operation runbooks should document which hops are considered „trusted“. - Timeouts: If a proxy has 30 seconds but your backend requires 2 minutes, you will produce ghost requests. Set timeouts consistently along the chain and decide: synchronous request or job pattern (202 Accepted + status endpoint).
- Correlation-ID: Put the correlation ID into response headers so operators can correlate logs and client-side traces. If a gateway uses its own request IDs: log and map both IDs.
- Error messages: In production do not expose internal details. Provide debug details only in a controlled manner (stage/feature-flag) and, if in doubt, only in the logs.
Context: Why RemObjects SDK can be advantageous here
In Delphi-ecosystems, REST-servers are often built with lighter-weight frameworks (e.g. minimalist HTTP routers). RemObjects SDK shows its strengths when you already have or require a multilayer architecture:
- Clear service boundaries: Service methods are explicit, contracts are versionable.
- Transports and serialization: You can speak JSON, but also other message formats (depending on setup), without polluting the domain logic.
- Operations: Hosting options and integration into existing Windows- and Linux-services can be planned, including clean rollouts.
The presented approach complements that with the parts often missing in day-to-day operations: uniform error objects, deterministic versioning, and correlatable logging. Especially for bespoke enterprise software with long lifecycles, this saves time on updates and on integrating external systems.
Conclusion: Is the effort worthwhile — and where does the approach break down?
Value arises when your REST interface not only „works“ but is operable in the long term: stable JSON contracts, versioning without URL sprawl, traceable errors and debugging without guesswork. This is exactly where the approach with context, Correlation-ID and centralized exception mapping in RemObjects SDK is strong.
Limits of application: If you only have a single, short-lived endpoint without integration partners, media-type versioning quickly feels like overengineering. Snapshot logging also only makes sense if you implement redaction and activation in a disciplined way. And: if your proxy stack „optimizes“ or removes headers, you must correct the infrastructure first, otherwise you’ll be debugging the wrong layer.
If you need to modernize an existing Delphi-server landscape or integrate a process-near software solution cleanly into ERP/DMS/CRM, these mechanisms are often the difference between „works in tests“ and „works in production“.
In the functional context, Delphi REST-API and REST-Server and Remobjects Sdk Delphi play an important role when integrations, data flows and further development must operate in concert.
Discuss a project or modernization initiative with Net-Base.
Next step
When the topic becomes an actual project, architecture, existing systems and operations should be considered together from the outset.
We support not only with individual issues, but also when source snippets, legacy topics, or portal ideas are to be turned into a robust enterprise project.
- Current state, target state and technical risks are assessed jointly.
- REST, data access, portals and rollout are not deferred to a later stage as secondary consequences.
- You can see early on which path is economically and operationally viable.