Skip to content

MARS Curiosity REST Server Integration

InstantObjects integrates with MARS Curiosity, a powerful and lightweight Delphi REST framework, to create professional REST APIs that expose your persistent business objects. MARS provides a flexible, attribute-based approach to building RESTful web services.

Overview

The MARS integration provides:

  • Automatic JSON serialization - Business objects to/from JSON using Neon
  • RESTful endpoints - Standard CRUD operations via HTTP
  • Resource base classes - Pre-built functionality for common patterns
  • JWT authentication - Token-based security with mORMot or JOSE
  • OpenAPI/Swagger support - Automatic API documentation
  • Flexible routing - Attribute-based URL mapping
  • Validation framework - Built-in request validation
  • Error handling - Structured exception handling
  • FireDAC integration - Direct database access when needed

Prerequisites

1. Install MARS Curiosity Framework

Download and install MARS from: https://github.com/andrea-magni/MARS

Add MARS library paths to your Delphi configuration.

2. Enable Neon Serialization

See JSON Serialization with Neon to enable DELPHI_NEON support.

3. Add InstantObjects MARS Units

Include these units from Source\MARSServer:

  • InstantObjects.Neon.MessageBodyReaders - JSON deserialization
  • InstantObjects.Neon.MessageBodyWriters - JSON serialization
  • InstantObjects.MARS.Server.Resources.Base - Base resource classes
  • InstantObjects.MARS.Server.Resources - Generic CRUD resources
  • InstantObjects.MARS.Server.Exceptions - Exception handling
  • InstantObjects.MARS.Data - InstantObjects data injection
  • InstantObjects.MARS.InjectionService - Dependency injection

Quick Start Example

1. Create MARS Server Application

pascal
program PrimerMARSServer;

uses
  System.SysUtils,
  MARS.http.Server.Indy,
  Server.Ignition;

begin
  TMARSWebServer.CreateServer
    .SetPort(8080)
    .SetThreadPoolSize(50)
    .Start;

  WriteLn('MARS Server running on http://localhost:8080/rest');
  WriteLn('OpenAPI documentation: http://localhost:8080/rest/openapi.json');
  WriteLn('Press ENTER to stop');
  ReadLn;

  TMARSWebServer.Default.Stop;
end.

2. Configure Engine (Server.Ignition.pas)

pascal
unit Server.Ignition;

interface

uses
  MARS.Core.Engine,
  MARS.Core.Engine.Interfaces;

const
  ENGINE_NAME = 'PrimerApi';
  APP_NAME = 'PrimerApp';
  APP_API_PATH = 'rest';

type
  TServerEngine = class
  private
    class var FEngine: IMARSEngine;
  public
    class constructor CreateEngine;
    class destructor DestroyEngine;
    class property Default: IMARSEngine read FEngine;
  end;

implementation

uses
  MARS.Core.Application,
  MARS.Core.MessageBodyWriters,
  MARS.Data.FireDAC,
  MARS.mORMotJWT.Token,
  MARS.OpenAPI.v3.InjectionService,
  InstantObjects.MARS.Data,
  InstantObjects.MARS.InjectionService,
  InstantObjects.Neon.MessageBodyReaders,
  InstantObjects.Neon.MessageBodyWriters,
  Primer.MARS.Server.Resources.User;  // Your resources

class constructor TServerEngine.CreateEngine;
var
  LApp: TMARSApplication;
begin
  FEngine := TMARSEngine.Create;
  FEngine.BasePath := APP_API_PATH;

  // Add application
  LApp := FEngine.AddApplication(APP_NAME, APP_BASE_PATH, [
    // Register your resources
    'Primer.MARS.Server.Resources.*'
  ]);

  // Register message body readers/writers
  LApp.AddMessageBodyWriters([
    'InstantObjects.Neon.MessageBodyWriters.*'
  ]);

  LApp.AddMessageBodyReaders([
    'InstantObjects.Neon.MessageBodyReaders.*'
  ]);

  // Register InstantObjects injection service
  LApp.RegisterInjectionService(TInstantObjectsInjectionService);

  // Register OpenAPI support
  FEngine.AddApplication('metadata', 'metadata', [
    'MARS.Metadata.Engine.Resource.*'
  ]);
end;

class destructor TServerEngine.DestroyEngine;
begin
  FEngine := nil;
end;

end.

3. Define Resource Class

pascal
unit Primer.MARS.Server.Resources.User;

interface

uses
  MARS.Core.Attributes,
  MARS.Core.MediaType,
  MARS.Core.Token,
  InstantObjects.MARS.Server.Resources,
  InstantObjects.MARS.Data,
  InstantPersistence,
  Model;

type
  [Path('/TUser')]
  [Produces(TMediaType.APPLICATION_JSON)]
  TUserResource = class(TInstantObjectResource)
  protected
    [Context] Token: TMARSToken;
    [Context] InstantObject: TMARSInstantObjects;
  public
    // GET /TUser - List all users
    [GET, Path('/')]
    [RolesAllowed('system')]
    function RetrieveUsersList(
      [QueryParam] Where: string = '';
      [QueryParam] OrderBy: string = ''): TInstantObjectList<TInstantObject>;

    // GET /TUser/{id} - Get single user
    [GET, Path('/{AId}')]
    [RolesAllowed('standard')]
    function RetrieveUser([PathParam] AId: string): TUser;

    // POST /TUser/{id} - Create user
    [POST, Path('/{AId}')]
    [Consumes(TMediaType.APPLICATION_JSON)]
    [RolesAllowed('system')]
    function PostUser(
      [PathParam] const AId: string;
      [BodyParam] AUser: TUser): TUser;

    // PUT /TUser/{id} - Update user
    [PUT, Path('/{AId}')]
    [Consumes(TMediaType.APPLICATION_JSON)]
    [RolesAllowed('system')]
    function PutUser(
      [PathParam] const AId: string;
      [BodyParam] AUser: TUser): TUser;

    // DELETE /TUser/{id} - Delete user
    [DELETE, Path('/{AId}')]
    [RolesAllowed('system')]
    function DeleteUser([PathParam] AId: string): TUser;

    // Custom endpoint
    [POST, Path('ChangePassword')]
    [Consumes(TMediaType.APPLICATION_JSON)]
    [RolesAllowed('standard')]
    function UserChangePassword(
      [FormParam('oldpassword')] const AOldPassword: string;
      [FormParam('newpassword')] const ANewPassword: string): string;
  end;

implementation

uses
  System.SysUtils,
  InstantObjects.MARS.Server.Exceptions;

{ TUserResource }

function TUserResource.RetrieveUsersList(
  Where, OrderBy: string): TInstantObjectList<TInstantObject>;
var
  Query: string;
begin
  Query := 'SELECT * FROM TUser';

  if Where <> '' then
    Query := Query + ' WHERE ' + Where;

  if OrderBy <> '' then
    Query := Query + ' ORDER BY ' + OrderBy;

  Result := TInstantObjectList<TInstantObject>.Create(False);
  InstantObject.RetrieveObjectList(Query, Result);
end;

function TUserResource.RetrieveUser(AId: string): TUser;
begin
  Result := TUser.Retrieve(AId);
  if not Assigned(Result) then
    raise EMARSHttpException.CreateFmt(404, 'User %s not found', [AId]);
end;

function TUserResource.PostUser(const AId: string; AUser: TUser): TUser;
begin
  // Validate
  if AUser.Email = '' then
    raise EMARSHttpException.Create(400, 'Email is required');

  // Set ID
  AUser.Id := AId;

  // Store
  AUser.Store;

  Result := AUser;
end;

function TUserResource.PutUser(const AId: string; AUser: TUser): TUser;
var
  ExistingUser: TUser;
begin
  ExistingUser := TUser.Retrieve(AId);
  if not Assigned(ExistingUser) then
    raise EMARSHttpException.CreateFmt(404, 'User %s not found', [AId]);

  try
    // Update fields from AUser
    ExistingUser.Email := AUser.Email;
    ExistingUser.FirstName := AUser.FirstName;
    ExistingUser.LastName := AUser.LastName;

    ExistingUser.Store;
    Result := ExistingUser;
  except
    ExistingUser.Free;
    raise;
  end;
end;

function TUserResource.DeleteUser(AId: string): TUser;
begin
  Result := TUser.Retrieve(AId);
  if not Assigned(Result) then
    raise EMARSHttpException.CreateFmt(404, 'User %s not found', [AId]);

  try
    Result.Dispose;
  except
    Result.Free;
    raise;
  end;
end;

function TUserResource.UserChangePassword(
  const AOldPassword, ANewPassword: string): string;
var
  User: TUser;
  UserId: string;
begin
  // Get current user from token
  UserId := Token.UserName;

  User := TUser.Retrieve(UserId);
  if not Assigned(User) then
    raise EMARSHttpException.Create(404, 'User not found');

  try
    // Verify old password
    if User.Password <> AOldPassword then
      raise EMARSHttpException.Create(401, 'Invalid old password');

    // Set new password
    User.Password := ANewPassword;
    User.Store;

    Result := 'Password changed successfully';
  finally
    User.Free;
  end;
end;

end.

Using Base Resource Classes

InstantObjects provides base classes for common patterns:

TBaseResource

Base class with utility methods for all resources:

pascal
TBaseResource = class
protected
  [Context] URL: TMARSURL;
  [Context] Request: IMARSRequest;
  [Context] Response: IMARSResponse;
  [Context] Token: TMARSToken;

  // Validation helpers
  procedure CheckRequiredField(const AFieldName: string; out AValue: string);
  procedure CheckRequiredIntegerField(const AFieldName: string; out AValue: Integer);
  procedure CheckStringMaxLength(const AFieldName, AValue: string; const AMaxLength: Integer);

  // InstantObjects access
  function GetInstantObjectClass(const AClassName: string): TInstantObjectClass;
end;

TInstantObjectResource

Generic resource for InstantObjects CRUD operations:

pascal
TInstantObjectResource = class(TBaseResource)
protected
  procedure AcceptedClassName(const AClassName: string; var AAccepted: boolean); virtual;

  function Post(const AId: string; AJSONBodyObject: TInstantObject;
    const ACheckIfExists: boolean = True): TInstantObject; virtual;

  function Put(const AId: string; AJSONBodyObject: TInstantObject): TInstantObject; virtual;

  function Delete(const AId: string; const AClassName: string = ''): TInstantObject; virtual;
end;

Advanced Features

JWT Authentication

pascal
// Token resource for login
[Path('/token')]
TTokenResource = class(TBaseResource)
public
  [POST, Path('/login')]
  [Consumes(TMediaType.APPLICATION_FORM_URLENCODED)]
  [Produces(TMediaType.APPLICATION_JSON)]
  function Login(
    [FormParam('username')] const AUsername: string;
    [FormParam('password')] const APassword: string): TJSONObject;
end;

implementation

function TTokenResource.Login(const AUsername, APassword: string): TJSONObject;
var
  User: TUser;
  Token: string;
begin
  User := ValidateCredentials(AUsername, APassword);
  if not Assigned(User) then
    raise EMARSHttpException.Create(401, 'Invalid credentials');

  try
    // Generate JWT token
    Token := TMARSToken.Build(
      AUsername,
      User.AccessRoles.Split([',']),  // Roles array
      TOKEN_SECRET,
      TOKEN_EXPIRATION_SECONDS
    );

    Result := TJSONObject.Create;
    Result.AddPair('token', Token);
    Result.AddPair('expires_in', TJSONNumber.Create(TOKEN_EXPIRATION_SECONDS));
  finally
    User.Free;
  end;
end;

OpenAPI/Swagger Documentation

MARS automatically generates OpenAPI documentation:

pascal
// Access at: http://localhost:8080/rest/openapi.json

// Or use Swagger UI:
// http://localhost:8080/rest/swagger-ui

Filtering and Query Parameters

pascal
[GET, Path('/')]
function GetContacts(
  [QueryParam] filter: string = '';
  [QueryParam] sort: string = '';
  [QueryParam] limit: Integer = 100;
  [QueryParam] offset: Integer = 0): TArray<TContact>;
var
  Query: string;
  Selector: TInstantSelector;
  List: TObjectList<TContact>;
begin
  Query := 'SELECT * FROM TContact';

  if filter <> '' then
    Query := Query + ' WHERE ' + filter;

  if sort <> '' then
    Query := Query + ' ORDER BY ' + sort;

  List := TObjectList<TContact>.Create(False);
  Selector := TInstantSelector.Create(nil);
  try
    Selector.Command.Text := Query;
    Selector.Open;

    // Apply pagination
    Selector.RecNo := offset;
    while (not Selector.EOF) and (List.Count < limit) do
    begin
      List.Add(Selector.CurrentObject as TContact);
      Selector.Next;
    end;

    Result := List.ToArray;
  finally
    Selector.Free;
    List.Free;
  end;
end;

Custom Exception Handling

pascal
type
  EIOResourceNotFound = class(EMARSHttpException)
  public
    constructor Create(const AMessage: string);
  end;

  EIOValidationError = class(EMARSHttpException)
  public
    constructor Create(const AMessage: string);
  end;

implementation

constructor EIOResourceNotFound.Create(const AMessage: string);
begin
  inherited Create(404, AMessage);
end;

constructor EIOValidationError.Create(const AMessage: string);
begin
  inherited Create(400, AMessage);
end;

// Usage
if not IsValidEmail(User.Email) then
  raise EIOValidationError.Create('Invalid email format');

Dependency Injection

MARS supports dependency injection for InstantObjects:

pascal
type
  TMyResource = class
  private
    [Context] InstantObject: TMARSInstantObjects;
    [Context] Token: TMARSToken;
  public
    function GetCurrentUser: TUser;
  end;

function TMyResource.GetCurrentUser: TUser;
begin
  Result := TUser.Retrieve(Token.UserName, False, False,
    InstantObject.Connector);
end;

Request/Response Interceptors

pascal
// Log all requests
TLoggingFilter = class(TInterfacedObject, IMARSContainerRequestFilter)
public
  procedure Filter(ARequestContext: TMARSRequestContext);
end;

procedure TLoggingFilter.Filter(ARequestContext: TMARSRequestContext);
begin
  Log.Info('Request: %s %s from %s', [
    ARequestContext.Request.Method,
    ARequestContext.Request.PathInfo,
    ARequestContext.Request.RemoteIP
  ]);
end;

// Register filter
LApp.SetFilters(['TLoggingFilter']);

Configuration and Deployment

Server Configuration

Configure via INI file (Primer.MARS.Console.ini):

ini
[Server]
Port=8080
ThreadPoolSize=50
BasePath=rest

[JWT]
Secret=your-secret-key-here
ExpirationSeconds=3600

[InstantObjects]
ConnectionDef=MyFireDACConnection
UseTransactions=true

CORS Configuration

pascal
procedure ConfigureCORS(AApp: TMARSApplication);
begin
  AApp.AddFilter('MARS.CORS.Filter.TCORSFilter')
    .AddParameter('AllowedOrigins', '*')
    .AddParameter('AllowedMethods', 'GET,POST,PUT,DELETE,OPTIONS')
    .AddParameter('AllowedHeaders', 'Content-Type,Authorization');
end;

SSL/TLS Support

pascal
var
  Server: TMARShttpServerIndy;
begin
  Server := TMARShttpServerIndy.Create(TServerEngine.Default);
  Server.DefaultPort := 443;
  Server.Engine := TServerEngine.Default;

  // Configure SSL
  Server.IOHandler := TIdServerIOHandlerSSLOpenSSL.Create(Server);
  with TIdServerIOHandlerSSLOpenSSL(Server.IOHandler) do
  begin
    SSLOptions.Method := sslvTLSv1_2;
    SSLOptions.CertFile := 'cert.pem';
    SSLOptions.KeyFile := 'key.pem';
  end;

  Server.Active := True;
end;

Testing REST Endpoints

Using cURL

bash
# Login to get token
curl -X POST http://localhost:8080/rest/primer/token/login \
  -d "username=admin&password=secret"

# Use token for authenticated requests
TOKEN="eyJhbGc..."
curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:8080/rest/primer/TUser/

# Create user
curl -X POST http://localhost:8080/rest/primer/TUser/USER001 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"john@example.com","firstName":"John"}'

# Get OpenAPI documentation
curl http://localhost:8080/rest/openapi.json

Using Postman

Import OpenAPI spec from http://localhost:8080/rest/openapi.json into Postman.

Demo Application

See the complete working example in Demos\PrimerRESTServer\:

  • Primer.MARS.Console.dpr - Console server application
  • Server.Ignition.pas - Server configuration and setup
  • Primer.MARS.Server.Resources.User.pas - User resource implementation
  • Primer.MARS.Server.Resources.Token.pas - Authentication resource
  • Primer.MARS.Console.ini - Configuration file

Run the demo:

cd Demos\PrimerRESTServer
dcc32 Primer.MARS.Console.dpr
Primer.MARS.Console.exe

Access the API at: http://localhost:8080/rest/primer

MARS vs WiRL Comparison

FeatureMARS CuriosityWiRL
MaturityEstablished, active developmentNewer, actively developed
DocumentationExtensiveGrowing
OpenAPI SupportBuilt-inVia plugins
JWT ImplementationmORMot or JOSEMultiple options
Learning CurveModerateGentle
PerformanceExcellentExcellent
CommunityLarge, activeGrowing

Choose MARS if:

  • You need comprehensive OpenAPI/Swagger support out of the box
  • You prefer mORMot JWT implementation
  • You want extensive documentation and examples
  • You're building enterprise-grade APIs

Choose WiRL if:

  • You prefer a simpler, more modern API
  • You want more flexibility in authentication
  • You're starting a new project and prefer newer frameworks

See WiRL REST Server for WiRL comparison.

Best Practices

  1. Use dependency injection - Leverage MARS context injection for clean code
  2. Implement proper authentication - Always use JWT for protected endpoints
  3. Validate all inputs - Use CheckRequiredField helpers
  4. Handle exceptions properly - Return appropriate HTTP status codes
  5. Document your API - Use OpenAPI annotations
  6. Version your API - Include version in base path (e.g., /api/v1/)
  7. Implement rate limiting - Protect against abuse
  8. Log everything - Use filters for request/response logging
  9. Use HTTPS in production - Always encrypt traffic
  10. Test thoroughly - Use automated testing with OpenAPI specs

Troubleshooting

Problem: "Class not found" error

  • Solution: Ensure resource class is registered in engine configuration

Problem: JWT token validation fails

  • Solution: Check token secret matches between generation and validation

Problem: JSON serialization errors

  • Solution: Verify Neon attributes are properly configured in model classes

Problem: CORS errors

  • Solution: Configure CORS filter with appropriate origins

Problem: OpenAPI spec not generating

  • Solution: Ensure metadata application is registered in engine

See Also

Released under Mozilla License, Version 2.0.