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 deserializationInstantObjects.Neon.MessageBodyWriters- JSON serializationInstantObjects.MARS.Server.Resources.Base- Base resource classesInstantObjects.MARS.Server.Resources- Generic CRUD resourcesInstantObjects.MARS.Server.Exceptions- Exception handlingInstantObjects.MARS.Data- InstantObjects data injectionInstantObjects.MARS.InjectionService- Dependency injection
Quick Start Example
1. Create MARS Server Application
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)
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
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:
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:
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
// 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:
// Access at: http://localhost:8080/rest/openapi.json
// Or use Swagger UI:
// http://localhost:8080/rest/swagger-uiFiltering and Query Parameters
[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
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:
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
// 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):
[Server]
Port=8080
ThreadPoolSize=50
BasePath=rest
[JWT]
Secret=your-secret-key-here
ExpirationSeconds=3600
[InstantObjects]
ConnectionDef=MyFireDACConnection
UseTransactions=trueCORS Configuration
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
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
# 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.jsonUsing 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 applicationServer.Ignition.pas- Server configuration and setupPrimer.MARS.Server.Resources.User.pas- User resource implementationPrimer.MARS.Server.Resources.Token.pas- Authentication resourcePrimer.MARS.Console.ini- Configuration file
Run the demo:
cd Demos\PrimerRESTServer
dcc32 Primer.MARS.Console.dpr
Primer.MARS.Console.exeAccess the API at: http://localhost:8080/rest/primer
MARS vs WiRL Comparison
| Feature | MARS Curiosity | WiRL |
|---|---|---|
| Maturity | Established, active development | Newer, actively developed |
| Documentation | Extensive | Growing |
| OpenAPI Support | Built-in | Via plugins |
| JWT Implementation | mORMot or JOSE | Multiple options |
| Learning Curve | Moderate | Gentle |
| Performance | Excellent | Excellent |
| Community | Large, active | Growing |
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
- Use dependency injection - Leverage MARS context injection for clean code
- Implement proper authentication - Always use JWT for protected endpoints
- Validate all inputs - Use CheckRequiredField helpers
- Handle exceptions properly - Return appropriate HTTP status codes
- Document your API - Use OpenAPI annotations
- Version your API - Include version in base path (e.g.,
/api/v1/) - Implement rate limiting - Protect against abuse
- Log everything - Use filters for request/response logging
- Use HTTPS in production - Always encrypt traffic
- 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
- JSON Serialization with Neon - JSON serialization details
- JSON Broker - File-based JSON storage
- WiRL REST Server - Alternative REST framework
- MARS Curiosity Documentation - Complete MARS reference
- MARS Curiosity Demos - Official MARS examples
