The Quick and Powerful Way
To implement a service in the Synopse mORMot 2 framework, the most direct approach is to define published methods on the server side, then use simple JSON or URL parameter handling to encode and decode requests on both ends.
This chapter covers method-based services — a straightforward, low-level mechanism for exposing custom functionality over HTTP. While mORMot 2 also provides interface-based services (covered in chapters 15-16) for more structured SOA, method-based services offer maximum flexibility and control.
On the server side, we customize a TRestServer descendant (typically TRestServerDB with SQLite3, or the lighter TRestServerFullMemory) by adding a new published method:
type
TRestServerTest = class(TRestServerFullMemory)
published
procedure Sum(Ctxt: TRestServerUriContext);
end;
The method name (Sum) determines the URI routing — it will be accessible remotely from ModelRoot/Sum. The ModelRoot is the Root parameter defined when creating the model.
All server-side methods MUST follow the TOnRestServerCallBack prototype:
type
TOnRestServerCallBack = procedure(Ctxt: TRestServerUriContext) of object;
The single Ctxt parameter provides full access to the execution context: incoming parameters, HTTP headers, session information, and output facilities.
procedure TRestServerTest.Sum(Ctxt: TRestServerUriContext);
begin
Ctxt.Results([Ctxt.InputDouble['a'] + Ctxt.InputDouble['b']]);
end;
The Ctxt object exposes typed properties for parameter retrieval:
| Property | Return Type | Exception on Missing |
|---|---|---|
InputInt['name'] |
Int64 |
Yes |
InputDouble['name'] |
Double |
Yes |
InputUtf8['name'] |
RawUtf8 |
Yes |
Input['name'] |
Variant |
Yes |
InputIntOrVoid['name'] |
Int64 |
No (returns 0) |
InputDoubleOrVoid['name'] |
Double |
No (returns 0) |
InputUtf8OrVoid['name'] |
RawUtf8 |
No (returns '') |
InputOrVoid['name'] |
Variant |
No (returns Unassigned) |
InputExists['name'] |
Boolean |
N/A |
Input['name'] array property (via variant) allows the concise syntax Ctxt['name'].
Ctxt.Results([]) encodes values as a JSON object with a "Result" member:
GET /root/Sum?a=3.12&b=4.2
Returns:
{"Result":7.32}
This is perfectly AJAX-friendly and compatible with any HTTP client.
Important: Method implementations MUST be thread-safe. The TRestServer.Uri method expects callbacks to handle thread safety internally. This design maximizes performance and scalability by allowing fine-grained resource locking.
For read-only operations, no locking may be needed. For shared state modifications:
procedure TRestServerTest.UpdateCounter(Ctxt: TRestServerUriContext);
begin
fCounterLock.Lock;
try
Inc(fCounter);
Ctxt.Results([fCounter]);
finally
fCounterLock.UnLock;
end;
end;
The client uses dedicated methods to call services by name with parameters:
function Sum(aClient: TRestClientUri; a, b: Double): Double;
var
err: Integer;
begin
Val(aClient.CallBackGetResult('sum', ['a', a, 'b', b]), Result, err);
end;
A cleaner approach encapsulates service calls in a dedicated client class:
type
TMyClient = class(TRestHttpClientSocket) // or TRestHttpClientWebSockets
public
function Sum(a, b: Double): Double;
end;
function TMyClient.Sum(a, b: Double): Double;
var
err: Integer;
begin
Val(CallBackGetResult('sum', ['a', a, 'b', b]), Result, err);
end;
TRestClientUri provides several methods for service invocation:
| Method | Purpose |
|---|---|
CallBackGetResult |
GET request, returns the JSON "Result" value as RawUtf8 |
CallBackGet |
GET request, returns full response with HTTP status |
CallBackPut |
PUT request with body data |
CallBack |
Generic request with any HTTP method |
function CallBackGet(const aMethodName: RawUtf8;
const aNameValueParameters: array of const;
out aResponse: RawUtf8;
aTable: TOrmClass = nil;
aID: TID = 0;
aResponseHead: PRawUtf8 = nil): Integer;
function CallBackGetResult(const aMethodName: RawUtf8;
const aNameValueParameters: array of const;
aTable: TOrmClass = nil;
aID: TID = 0): RawUtf8;
Objects can be serialized to JSON automatically:
function TMyClient.ProcessPerson(Person: TPerson): RawUtf8;
begin
Result := CallBackGetResult('processperson', ['person', ObjectToJson(Person)]);
end;
For maximum performance, bypass the high-level Input[] properties and parse Ctxt.Parameters directly:
procedure TRestServerTest.Sum(Ctxt: TRestServerUriContext);
var
a, b: Double;
begin
if UrlDecodeNeedParameters(Ctxt.Parameters, 'A,B') then
begin
while Ctxt.Parameters <> nil do
begin
UrlDecodeDouble(Ctxt.Parameters, 'A=', a);
UrlDecodeDouble(Ctxt.Parameters, 'B=', b, @Ctxt.Parameters);
end;
Ctxt.Results([a + b]);
end
else
Ctxt.Error('Missing Parameter');
end;
Available in mormot.core.text:
| Function | Purpose |
|---|---|
UrlDecodeNeedParameters |
Verify required parameters exist |
UrlDecodeInteger |
Extract integer parameter |
UrlDecodeInt64 |
Extract 64-bit integer |
UrlDecodeDouble |
Extract floating-point |
UrlDecodeValue |
Extract string value |
UrlDecodeObject |
Deserialize JSON to object |
For POST/PUT requests, the body is available in Ctxt.Call^.InBody:
procedure TRestServerTest.ProcessData(Ctxt: TRestServerUriContext);
var
doc: TDocVariantData;
begin
if doc.InitJson(Ctxt.Call^.InBody, JSON_FAST) then
begin
// Process doc...
Ctxt.Success;
end
else
Ctxt.Error('Invalid JSON body');
end;
Use Ctxt.Returns() to return any content type:
procedure TRestServer.Timestamp(Ctxt: TRestServerUriContext);
begin
Ctxt.Returns(Int64ToUtf8(ServerTimestamp), HTTP_SUCCESS, TEXT_CONTENT_TYPE_HEADER);
end;
procedure TRestServer.GetFile(Ctxt: TRestServerUriContext);
var
fileName: TFileName;
content: RawByteString;
begin
fileName := 'c:\data\' + ExtractFileName(Utf8ToString(Ctxt.InputUtf8['filename']));
content := StringFromFile(fileName);
if content = '' then
Ctxt.Error('', HTTP_NOTFOUND)
else
Ctxt.Returns(content, HTTP_SUCCESS,
HEADER_CONTENT_TYPE + GetMimeContentType(pointer(content), Length(content), fileName));
end;
function TMyClient.GetFile(const aFileName: RawUtf8): RawByteString;
var
resp: RawUtf8;
begin
if CallBackGet('GetFile', ['filename', aFileName], resp) <> HTTP_SUCCESS then
raise Exception.CreateFmt('Impossible to get file: %s', [resp]);
Result := RawByteString(resp);
end;
Note: For file serving, prefer Ctxt.ReturnFile() or Ctxt.ReturnFileFromFolder() (covered in section 14.7).
Methods can be linked to ORM tables via RESTful URIs like ModelRoot/TableName/TableID/MethodName:
procedure TRestServerTest.DataAsHex(Ctxt: TRestServerUriContext);
var
aData: RawBlob;
begin
if (Self = nil) or (Ctxt.Table <> TOrmPeople) or (Ctxt.TableID <= 0) then
Ctxt.Error('Need a valid record and its ID')
else if (Ctxt.Server.Orm as TRestOrmServer).RetrieveBlob(
TOrmPeople, Ctxt.TableID, 'Data', aData) then
Ctxt.Results([BinToHex(aData)])
else
Ctxt.Error('Impossible to retrieve the Data BLOB field');
end;
Corresponding client call:
function TOrmPeople.DataAsHex(aClient: TRestClientUri): RawUtf8;
begin
Result := aClient.CallBackGetResult('DataAsHex', [], TOrmPeople, ID);
end;
The TRestServerUriContext exposes rich information:
| Property | Description |
|---|---|
Table |
TOrmClass decoded from URI |
TableIndex |
Index in Server.Model |
TableID |
Record ID from URI |
Session |
Session ID (0 = not started, 1 = auth disabled) |
SessionUser |
Current user's TID |
SessionGroup |
Current group's TID |
SessionUserName |
User's logon name |
Method |
HTTP verb (mGET, mPOST, etc.) |
Call^.InHead |
Raw incoming HTTP headers |
Call^.InBody |
Raw request body |
RemoteIP |
Client IP address |
UserAgent |
Client user-agent string |
procedure TRestServerTest.WhoAmI(Ctxt: TRestServerUriContext);
var
User: TAuthUser;
begin
if Ctxt.Session = CONST_AUTHENTICATION_NOT_USED then
Ctxt.Returns(['message', 'Authentication not enabled'])
else if Ctxt.Session = CONST_AUTHENTICATION_SESSION_NOT_STARTED then
Ctxt.Returns(['message', 'Not authenticated'])
else
begin
User := Ctxt.Server.SessionGetUser(Ctxt.Session);
try
if User <> nil then
Ctxt.Returns(['user', User.LogonName, 'group', Ctxt.SessionGroup])
else
Ctxt.Error('Session not found', HTTP_FORBIDDEN);
finally
User.Free;
end;
end;
end;
The optional Handle304NotModified parameter enables browser caching:
procedure TRestServerTest.GetStaticData(Ctxt: TRestServerUriContext);
var
data: RawUtf8;
begin
data := GetCachedData; // Your cached data source
Ctxt.Returns(data, HTTP_SUCCESS, JSON_CONTENT_TYPE_HEADER, true); // Handle304NotModified=true
end;
When enabled:
crc32c (with SSE4.2 hardware acceleration if available)304 Not Modified without bodyServer.ServiceMethodByPassAuthentication() to disable authentication for cached methods.crc32c, false positives are theoretically possible. Don't use for sensitive accounting data.This stateless REST model enables multiple levels of caching:
Ctxt.ReturnFile() efficiently serves files with automatic MIME type detection:
procedure TRestServerTest.DownloadReport(Ctxt: TRestServerUriContext);
begin
Ctxt.ReturnFile('c:\reports\' + Ctxt.InputUtf8['name'] + '.pdf', true);
end;
Features:
Handle304NotModified using file timestampServes any file from a folder based on the URI path:
procedure TRestServerTest.StaticFiles(Ctxt: TRestServerUriContext);
begin
Ctxt.ReturnFileFromFolder('c:\www\static\', true, 'index.html', '/404.html');
end;
Parameters:
FolderName: Base folder pathHandle304NotModified: Enable browser cachingDefaultFileName: Served for root requests (default: 'index.html')Error404Redirect: Redirect URI for missing files
JSON Web Tokens (JWT) provide stateless authentication and secure information exchange. mORMot 2 implements JWT in mormot.crypt.jwt:
Supported Algorithms:
| Algorithm | Description |
|---|---|
HS256/384/512 |
HMAC-SHA2 (symmetric) |
ES256 |
ECDSA P-256 (asymmetric) |
RS256/384/512 |
RSA (asymmetric) |
PS256/384/512 |
RSA-PSS (asymmetric) |
S3256/384/512 |
SHA-3 (non-standard) |
none |
No signature (use with caution) |
TJwtAbstract
├── TJwtNone (algorithm: "none")
├── TJwtSynSignerAbstract
│ ├── TJwtHS256/384/512 (HMAC-SHA2)
│ └── TJwtS3256/384/512 (SHA-3) │
├── TJwtES256 (ECDSA P-256)
├── TJwtRS256/384/512 (RSA)
├── TJwtPS256/384/512 (RSA-PSS)
└── TJwtCrypt (factory-based, recommended)
uses
mormot.crypt.jwt;
var
jwt: TJwtAbstract;
content: TJwtContent;
begin
jwt := TJwtHS256.Create('secret', 0, [jrcSubject], []);
try
jwt.Verify(
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' +
'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.' +
'TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ',
content);
Assert(content.result = jwtValid);
Assert(content.reg[jrcSubject] = '1234567890');
Assert(content.data.U['name'] = 'John Doe');
Assert(content.data.B['admin'] = True);
finally
jwt.Free;
end;
end;
var
jwt: TJwtAbstract;
token: RawUtf8;
begin
jwt := TJwtHS256.Create('secret', 10000, // 10000 PBKDF2 rounds
[jrcIssuer, jrcExpirationTime, jrcIssuedAt, jrcJWTID], [], 3600); // 1 hour expiry
try
token := jwt.Compute(['http://example.com/is_root', True], 'joe');
// token payload: {"http://example.com/is_root":true,"iss":"joe","iat":...,"exp":...,"jti":...}
finally
jwt.Free;
end;
end;
Integrate JWT validation using Ctxt.AuthenticationCheck():
type
TMyDaemon = class(TRestServerFullMemory)
private
fJwt: TJwtAbstract;
public
constructor Create(aModel: TOrmModel);
published
procedure SecureFiles(Ctxt: TRestServerUriContext);
end;
constructor TMyDaemon.Create(aModel: TOrmModel);
begin
inherited Create(aModel);
fJwt := TJwtHS256.Create('my-secret-key', 10000, [jrcSubject], [], 3600);
end;
procedure TMyDaemon.SecureFiles(Ctxt: TRestServerUriContext);
begin
if Ctxt.AuthenticationCheck(fJwt) = jwtValid then
Ctxt.ReturnFileFromFolder('c:\protected\')
else
; // AuthenticationCheck already returned HTTP 401
end;
Assign a JWT instance to handle all unauthenticated requests:
Server.JwtForUnauthenticatedRequest := TJwtHS256.Create('secret', 10000, [], []);
Missing parameters in Input[] properties raise EParsingException, which the server catches and returns as a structured error response:
{
"ErrorCode": 400,
"ErrorText": "EParsingException: Missing parameter 'name'"
}
Use Ctxt.Error() for custom error responses:
procedure TRestServer.UpdateRecord(Ctxt: TRestServerUriContext);
begin
if not CanUpdate(Ctxt.InputInt['id']) then
Ctxt.Error('Record is locked', HTTP_FORBIDDEN)
else if DoUpdate(Ctxt.InputInt['id'], Ctxt.InputUtf8['data']) then
Ctxt.Success
else
Ctxt.Error('Update failed', HTTP_SERVERERROR);
end;
For operations that don't return data:
procedure TRestServer.DeleteItem(Ctxt: TRestServerUriContext);
begin
if DoDelete(Ctxt.InputInt['id']) then
Ctxt.Success // Returns HTTP 200 with empty body
else
Ctxt.Error('Delete failed');
end;
| Method | Default Status |
|---|---|
Ctxt.Results() |
200 OK |
Ctxt.Returns() |
200 OK (customizable) |
Ctxt.Success() |
200 OK (customizable) |
Ctxt.Error() |
400 Bad Request (customizable) |
HTTP_SUCCESS = 200HTTP_CREATED = 201HTTP_NOCONTENT = 204HTTP_BADREQUEST = 400HTTP_FORBIDDEN = 403HTTP_NOTFOUND = 404HTTP_SERVERERROR = 500
Certain methods (like Timestamp or public API endpoints) should be accessible without authentication:
Server.ServiceMethodByPassAuthentication('Timestamp');
Server.ServiceMethodByPassAuthentication('GetPublicData');
Restrict which HTTP verbs are allowed for a method:
// In TRestServerMethod, set during initialization
Server.PublishedMethods['MyMethod'].Methods := [mGET, mPOST];
Method-based services provide:
| Benefit | Description |
|---|---|
| Full control | Direct access to HTTP headers, binary data, custom MIME types |
| RESTful integration | Can be linked to ORM tables via URI routing |
| Low overhead | Minimal abstraction layer, maximum performance |
| Flexibility | Handle any request type (AJAX, SOAP, custom protocols) |
| Simple debugging | Direct mapping between URI and code |
The mORMot implementation is inherently secure against certain attacks:
| Limitation | Solution |
|---|---|
| Manual parameter marshalling | Use interface-based services (Chapter 16) |
| No automatic client stub generation | Use interface-based services |
| Flat service namespace | Organize via naming conventions or interfaces |
| No automatic documentation | Generate manually or use OpenAPI export |
Use method-based services when:
unit RestServerUnit;
interface
uses
mormot.core.base,
mormot.core.text,
mormot.core.json,
mormot.orm.core,
mormot.rest.core,
mormot.rest.server,
mormot.rest.memserver;
type
TMyRestServer = class(TRestServerFullMemory)
published
procedure Sum(Ctxt: TRestServerUriContext);
procedure Echo(Ctxt: TRestServerUriContext);
procedure Time(Ctxt: TRestServerUriContext);
end;
implementation
procedure TMyRestServer.Sum(Ctxt: TRestServerUriContext);
begin
Ctxt.Results([Ctxt.InputDouble['a'] + Ctxt.InputDouble['b']]);
end;
procedure TMyRestServer.Echo(Ctxt: TRestServerUriContext);
begin
Ctxt.Returns(Ctxt.Call^.InBody, HTTP_SUCCESS, TEXT_CONTENT_TYPE_HEADER);
end;
procedure TMyRestServer.Time(Ctxt: TRestServerUriContext);
begin
Ctxt.Returns(['timestamp', ServerTimestamp, 'utc', DateTimeToIso8601(NowUtc, true)]);
end;
end.
unit RestClientUnit;
interface
uses
mormot.core.base,
mormot.rest.client,
mormot.rest.http.client;
type
TMyRestClient = class(TRestHttpClientSocket)
public
function Sum(a, b: Double): Double;
function Echo(const Text: RawUtf8): RawUtf8;
function GetServerTime: TDateTime;
end;
implementation
uses
mormot.core.json;
function TMyRestClient.Sum(a, b: Double): Double;
var
err: Integer;
begin
Val(CallBackGetResult('sum', ['a', a, 'b', b]), Result, err);
end;
function TMyRestClient.Echo(const Text: RawUtf8): RawUtf8;
var
resp: RawUtf8;
begin
if CallBack(mPOST, 'echo', Text, resp) = HTTP_SUCCESS then
Result := resp
else
Result := '';
end;
function TMyRestClient.GetServerTime: TDateTime;
var
doc: TDocVariantData;
resp: RawUtf8;
begin
if CallBackGet('time', [], resp) = HTTP_SUCCESS then
begin
doc.InitJson(resp, JSON_FAST);
Result := Iso8601ToDateTime(doc.U['utc']);
end
else
Result := 0;
end;
end.
program MethodServicesDemo;
uses
mormot.core.base,
mormot.orm.core,
mormot.rest.http.server,
RestServerUnit,
RestClientUnit;
var
Model: TOrmModel;
Server: TMyRestServer;
HttpServer: TRestHttpServer;
Client: TMyRestClient;
begin
Model := TOrmModel.Create([], 'root');
Server := TMyRestServer.Create(Model);
try
Server.ServiceMethodByPassAuthentication('Time');
HttpServer := TRestHttpServer.Create('8080', [Server], '+', useHttpAsync);
try
// Client demo
Client := TMyRestClient.Create('localhost', '8080', TOrmModel.Create([], 'root'));
try
WriteLn('Sum(3.5, 2.5) = ', Client.Sum(3.5, 2.5):0:2);
WriteLn('Echo: ', Client.Echo('Hello mORMot!'));
WriteLn('Server time: ', DateTimeToStr(Client.GetServerTime));
finally
Client.Free;
end;
WriteLn('Press Enter to stop...');
ReadLn;
finally
HttpServer.Free;
end;
finally
Server.Free;
Model.Free;
end;
end.
Method-based services in mORMot 2 provide:
| Previous | Index | Next |
|---|---|---|
| Chapter 13: Server-Side ORM Processing | Index | Chapter 15: Interfaces and SOLID Design |