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.