Skip to content

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 provider
  • InstantObjects.WiRL.Server.Resources.Base - Base resource class
  • InstantObjects.WiRL.Server.Resources - Generic CRUD resources
  • InstantObjects.WiRL.Server.Exceptions - Exception handling
  • InstantObjects.WiRL.Data - Data handling utilities

Quick Start Example ​

1. Create WiRL Server Application ​

pascal
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 ​

pascal
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 ​

pascal
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:

pascal
[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:

pascal
[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 ​

pascal
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 ​

pascal
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 ​

pascal
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 ​

pascal
// 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 ​

pascal
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 ​

pascal
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 ​

pascal
App.SetFilters([
  TCORSFilter.Create('*')  // Allow all origins
]);

SSL/TLS Support ​

pascal
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 ​

bash
# 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/USER001

Using 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 application
  • Primer.WiRL.Server.Resources.User.pas - User resource implementation
  • Primer.WiRL.Server.Resources.Config.pas - Configuration resource
  • Primer.WiRL.Server.Listener.pas - Server setup and configuration

Run the demo:

cd Demos\PrimerWiRLServer
dcc32 Primer.WiRL.Console.dpr
Primer.WiRL.Console.exe

Access the API at: http://localhost:8080/api

Best Practices ​

  1. Use DTOs when needed - Separate API models from domain models for complex scenarios
  2. Implement pagination - Always limit result sets with offset/limit parameters
  3. Validate input - Never trust client data, always validate before persistence
  4. Handle transactions - Use database transactions for multi-object operations
  5. Log requests - Audit all API access for security and debugging
  6. Version your API - Use URL versioning (/api/v1/) for backwards compatibility
  7. Secure endpoints - Use JWT authentication for protected resources
  8. 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 ​

Released under Mozilla License, Version 2.0.