WiRL REST Server Integration
InstantObjects integrates seamlessly with WiRL, a powerful Delphi REST framework, to create modern REST APIs that expose your persistent business objects. This integration combines the simplicity of InstantObjects with the power of RESTful web services.
Overview
The WiRL 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 operations
- Authentication/Authorization - JWT token support and claims-based security
- Validation - Server-side validation before persistence
- Error handling - Structured exception handling and error responses
- Logging - Request/response logging and auditing
Prerequisites
1. Install WiRL Framework
Download and install WiRL from: https://wirl.delphiblocks.dev/
Add WiRL library paths to your Delphi configuration.
2. Enable Neon Serialization
See JSON Serialization with Neon to enable DELPHI_NEON support.
3. Add InstantObjects WiRL Units
Include these units from Source\WiRLServer:
InstantObjects.Neon.MessageBodyProvider- JSON serialization providerInstantObjects.WiRL.Server.Resources.Base- Base resource classInstantObjects.WiRL.Server.Resources- Generic CRUD resourcesInstantObjects.WiRL.Server.Exceptions- Exception handlingInstantObjects.WiRL.Data- Data handling utilities
Quick Start Example
1. Create WiRL Server Application
program PrimerWiRLServer;
uses
System.SysUtils,
WiRL.http.Server,
WiRL.http.Server.Indy,
WiRL.Core.Engine,
WiRL.Core.Application,
Primer.WiRL.Server.Listener,
Primer.WiRL.Server.Resources.User; // Your resource units
begin
TWiRLServer.Create;
try
Server.Port := 8080;
Server.Start;
WriteLn('WiRL Server running on http://localhost:8080');
WriteLn('Press ENTER to stop');
ReadLn;
Server.Stop;
finally
Server.Free;
end;
end;2. Define Resource Class
unit Primer.WiRL.Server.Resources.User;
interface
uses
WiRL.Core.Attributes,
WiRL.http.Accept.MediaType,
InstantObjects.WiRL.Server.Resources.Base,
Model; // Your business model
type
[Path('/users')]
TUserResource = class(TInstantObjectResource<TUser>)
public
// GET /users - List all users
[GET]
[Produces(TMediaType.APPLICATION_JSON)]
function GetUsers: TArray<TUser>;
// GET /users/{id} - Get single user
[GET, Path('/{id}')]
[Produces(TMediaType.APPLICATION_JSON)]
function GetUser([PathParam] id: string): TUser;
// POST /users - Create new user
[POST]
[Consumes(TMediaType.APPLICATION_JSON)]
[Produces(TMediaType.APPLICATION_JSON)]
function CreateUser([BodyParam] AUser: TUser): TUser;
// PUT /users/{id} - Update user
[PUT, Path('/{id}')]
[Consumes(TMediaType.APPLICATION_JSON)]
[Produces(TMediaType.APPLICATION_JSON)]
function UpdateUser([PathParam] id: string; [BodyParam] AUser: TUser): TUser;
// DELETE /users/{id} - Delete user
[DELETE, Path('/{id}')]
function DeleteUser([PathParam] id: string): Boolean;
end;
implementation
{ TUserResource }
function TUserResource.GetUsers: TArray<TUser>;
var
UserList: TObjectList<TUser>;
Selector: TInstantSelector;
begin
UserList := TObjectList<TUser>.Create(False);
Selector := TInstantSelector.Create(nil);
try
Selector.Command.Text := 'SELECT * FROM TUser';
Selector.Open;
while not Selector.EOF do
begin
UserList.Add(Selector.CurrentObject as TUser);
Selector.Next;
end;
Result := UserList.ToArray;
finally
Selector.Free;
UserList.Free;
end;
end;
function TUserResource.GetUser(id: string): TUser;
begin
Result := TUser.Retrieve(id);
if not Assigned(Result) then
raise EIOResourceNotFound.CreateFmt('User %s not found', [id]);
end;
function TUserResource.CreateUser(AUser: TUser): TUser;
begin
// Validate
if AUser.Email = '' then
raise EIOValidationError.Create('Email is required');
// Generate ID if needed
if AUser.Id = '' then
AUser.Id := InstantGenerateId;
// Store
AUser.Store;
Result := AUser;
end;
function TUserResource.UpdateUser(id: string; AUser: TUser): TUser;
var
ExistingUser: TUser;
begin
// Retrieve existing
ExistingUser := TUser.Retrieve(id);
if not Assigned(ExistingUser) then
raise EIOResourceNotFound.CreateFmt('User %s not found', [id]);
try
// Update fields
ExistingUser.Email := AUser.Email;
ExistingUser.FirstName := AUser.FirstName;
ExistingUser.LastName := AUser.LastName;
// Store changes
ExistingUser.Store;
Result := ExistingUser;
except
ExistingUser.Free;
raise;
end;
end;
function TUserResource.DeleteUser(id: string): Boolean;
var
User: TUser;
begin
User := TUser.Retrieve(id);
if not Assigned(User) then
raise EIOResourceNotFound.CreateFmt('User %s not found', [id]);
try
User.Dispose;
Result := True;
finally
User.Free;
end;
end;
end.3. Register Resources
procedure ConfigureWiRL(AEngine: TWiRLEngine);
var
App: TWiRLApplication;
begin
// Create application
App := AEngine.AddApplication('/api');
// Register message body providers
App.Plugin.Configure<IWiRLConfigurationNeon>
.SetUseUTCDate(True)
.SetMemberCase(TNeonCase.LowerCase)
.SetVisibility([mvPublic, mvPublished])
.BackToConfig
.ConfigureNeonProvider;
// Register resources
App.SetResources([
TUserResource,
TContactResource,
TCompanyResource
]);
// Register filters
App.SetFilters([
TAuthenticationFilter,
TLoggingFilter
]);
end;Using Base Resource Classes
InstantObjects provides base classes that implement common REST patterns:
TInstantObjectResource<T>
Generic base class for single-object resources:
[Path('/contacts')]
TContactResource = class(TInstantObjectResource<TContact>)
public
// Inherited methods you can override:
// - function DoRetrieve(const AId: string): T;
// - function DoCreate(AObject: T): T;
// - function DoUpdate(const AId: string; AObject: T): T;
// - function DoDelete(const AId: string): Boolean;
// - procedure ValidateObject(AObject: T); virtual;
end;TInstantObjectListResource<T>
Generic base class for collection resources:
[Path('/contacts')]
TContactListResource = class(TInstantObjectListResource<TContact>)
public
[GET]
[Produces(TMediaType.APPLICATION_JSON)]
function GetContacts(
[QueryParam('filter')] AFilter: string;
[QueryParam('sort')] ASort: string;
[QueryParam('limit')] ALimit: Integer = 100;
[QueryParam('offset')] AOffset: Integer = 0): TArray<TContact>;
end;Advanced Features
Filtering and Pagination
function TContactListResource.GetContacts(
AFilter, ASort: string; ALimit, AOffset: Integer): TArray<TContact>;
var
Query: string;
begin
Query := 'SELECT * FROM TContact';
if AFilter <> '' then
Query := Query + ' WHERE ' + AFilter;
if ASort <> '' then
Query := Query + ' ORDER BY ' + ASort;
Result := ExecuteQuery<TContact>(Query, ALimit, AOffset);
end;Authentication with JWT
type
[Path('/secure/users')]
TSecureUserResource = class(TUserResource)
private
[Context] FToken: TWiRLAuthContext;
public
[GET]
[Produces(TMediaType.APPLICATION_JSON)]
[RolesAllowed('admin,manager')]
function GetUsers: TArray<TUser>; override;
end;Custom Validation
procedure TUserResource.ValidateObject(AUser: TUser);
begin
inherited;
if AUser.Email = '' then
raise EIOValidationError.Create('Email is required');
if not IsValidEmail(AUser.Email) then
raise EIOValidationError.Create('Invalid email format');
if AUser.Password.Length < 8 then
raise EIOValidationError.Create('Password must be at least 8 characters');
// Check uniqueness
if UserEmailExists(AUser.Email, AUser.Id) then
raise EIOValidationError.Create('Email already in use');
end;Error Handling
// Custom exceptions automatically converted to HTTP responses
type
EIOResourceNotFound = class(EWiRLNotFoundException);
EIOValidationError = class(EWiRLBadRequestException);
EIOUnauthorized = class(EWiRLNotAuthorizedException);
// Exception mapper
procedure HandleException(E: Exception; AResponse: TWiRLResponse);
begin
if E is EIOResourceNotFound then
AResponse.StatusCode := 404
else if E is EIOValidationError then
AResponse.StatusCode := 400
else if E is EIOUnauthorized then
AResponse.StatusCode := 401
else
AResponse.StatusCode := 500;
AResponse.Content := TJSONObject.Create
.AddPair('error', E.Message)
.ToString;
end;Logging and Auditing
type
TLoggingFilter = class(TInterfacedObject, IWiRLContainerRequestFilter)
public
procedure Filter(ARequest: TWiRLRequest);
end;
procedure TLoggingFilter.Filter(ARequest: TWiRLRequest);
begin
Log.Info('Request: %s %s from %s',
[ARequest.Method, ARequest.PathInfo, ARequest.RemoteIP]);
// Log to database
TAuditLog.Create
.SetMethod(ARequest.Method)
.SetPath(ARequest.PathInfo)
.SetIP(ARequest.RemoteIP)
.SetTimestamp(Now)
.Store;
end;Configuration and Deployment
Server Configuration
var
Server: TWiRLServer;
Engine: TWiRLEngine;
begin
Server := TWiRLServer.Create(nil);
try
Server.Port := 8080;
Server.ThreadPoolSize := 50;
Engine := Server.AddEngine<TWiRLEngine>('/rest');
ConfigureWiRL(Engine);
Server.Start;
except
Server.Free;
raise;
end;
end;CORS Configuration
App.SetFilters([
TCORSFilter.Create('*') // Allow all origins
]);SSL/TLS Support
Server.Port := 443;
Server.SSLContext.SSLOptions.Method := sslvTLSv1_2;
Server.SSLContext.SSLOptions.CertFile := 'cert.pem';
Server.SSLContext.SSLOptions.KeyFile := 'key.pem';Testing REST Endpoints
Using cURL
# GET all users
curl http://localhost:8080/api/users
# GET single user
curl http://localhost:8080/api/users/USER001
# POST new user
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"email":"john@example.com","firstName":"John","lastName":"Doe"}'
# PUT update user
curl -X PUT http://localhost:8080/api/users/USER001 \
-H "Content-Type: application/json" \
-d '{"email":"john.doe@example.com"}'
# DELETE user
curl -X DELETE http://localhost:8080/api/users/USER001Using Postman
Import the collection from Demos\PrimerWiRLServer\Postman\ folder.
Demo Application
See the complete working example in Demos\PrimerWiRLServer\:
Primer.WiRL.Console.dpr- Console server applicationPrimer.WiRL.Server.Resources.User.pas- User resource implementationPrimer.WiRL.Server.Resources.Config.pas- Configuration resourcePrimer.WiRL.Server.Listener.pas- Server setup and configuration
Run the demo:
cd Demos\PrimerWiRLServer
dcc32 Primer.WiRL.Console.dpr
Primer.WiRL.Console.exeAccess the API at: http://localhost:8080/api
Best Practices
- Use DTOs when needed - Separate API models from domain models for complex scenarios
- Implement pagination - Always limit result sets with offset/limit parameters
- Validate input - Never trust client data, always validate before persistence
- Handle transactions - Use database transactions for multi-object operations
- Log requests - Audit all API access for security and debugging
- Version your API - Use URL versioning (/api/v1/) for backwards compatibility
- Secure endpoints - Use JWT authentication for protected resources
- Document API - Generate OpenAPI/Swagger documentation
Troubleshooting
Problem: JSON serialization fails
- Solution: Ensure DELPHI_NEON is defined and Neon attributes are properly set
Problem: Objects not persisted
- Solution: Check connector is connected and IsDefault is True
Problem: CORS errors in browser
- Solution: Add CORS filter with appropriate origin configuration
Problem: Authentication fails
- Solution: Verify JWT token format and claims configuration
See Also
- JSON Serialization with Neon - JSON serialization details
- JSON Broker - File-based JSON storage
- MARS Curiosity REST Server - Alternative REST framework
- WiRL Documentation - Complete WiRL reference
