Tina4

Chapter 2: REST APIs#

Two Ways to Talk to the Outside World#

Your application needs data from somewhere. A customer database behind an API. A payment gateway. A weather service. A machine learning endpoint. Between your Delphi form and that data sits HTTP -- and two very different ways to make the call.

The first way is component-based. Drop TTina4REST and TTina4RESTRequest on your form. Set properties. Execute. The MemTable fills. You write no HTTP code, no JSON parsing, no threading boilerplate.

The second way is direct. Call Tina4REST1.Get() or Tina4REST1.Post() and get a TJSONObject back. Full control. Full responsibility -- including freeing the object.

This chapter covers both.


1. TTina4REST -- Base Configuration#

Every REST call needs a server. TTina4REST holds that configuration so you set it once and every TTina4RESTRequest linked to it inherits the connection details.

Design-Time Setup#

Drop a TTina4REST on your form. In the Object Inspector:

PropertyDescriptionExample
BaseUrlThe root URL for all endpointshttps://api.example.com/v1
UsernameHTTP Basic Auth usernameadmin
PasswordHTTP Basic Auth passwordsecret

Runtime Configuration#

pascal
Tina4REST1.BaseUrl := 'https://api.example.com/v1';Tina4REST1.Username := 'admin';Tina4REST1.Password := 'secret';

Bearer Token Authentication#

Most modern APIs use Bearer tokens instead of Basic Auth. Call SetBearer after obtaining your token:

pascal
Tina4REST1.SetBearer('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');

This adds an Authorization: Bearer <token> header to every request made through this component. If you also set Username and Password, Basic Auth is used instead -- Bearer and Basic Auth are mutually exclusive.

One Component Per API#

If your application talks to multiple APIs, use multiple TTina4REST components:

pascal
// API 1: Your backendTina4RESTBackend.BaseUrl := 'https://api.myapp.com/v1';Tina4RESTBackend.SetBearer(AuthToken);โ€‹// API 2: Payment gatewayTina4RESTPayments.BaseUrl := 'https://payments.stripe.com';Tina4RESTPayments.SetBearer(StripeKey);โ€‹// API 3: Public dataTina4RESTPublic.BaseUrl := 'https://api.open-meteo.com';// No auth needed

2. Direct REST Calls#

When you need full control over the request and response, call methods directly on TTina4REST. All five HTTP methods are supported. All return a TJSONObject. All require you to free the result.

GET#

pascal
var  StatusCode: Integer;  Response: TJSONObject;begin  Response := Tina4REST1.Get(StatusCode, '/users', 'page=1&limit=10');  try    if StatusCode = 200 then      Memo1.Lines.Text := Response.Format    else      ShowMessage('Error: ' + StatusCode.ToString);  finally    Response.Free;  end;end;

The three parameters are: StatusCode (out), EndPoint, and QueryParams. The endpoint is appended to the BaseUrl. Query params are appended after a ?.

POST#

pascal
var  StatusCode: Integer;  Response: TJSONObject;begin  Response := Tina4REST1.Post(StatusCode, '/users', '',    '{"name": "Alice", "email": "alice@example.com"}');  try    if StatusCode = 201 then      ShowMessage('User created: ' + Response.GetValue<String>('id'))    else      ShowMessage('Failed: ' + Response.ToString);  finally    Response.Free;  end;end;

PATCH (Partial Update)#

pascal
var  StatusCode: Integer;  Response: TJSONObject;begin  Response := Tina4REST1.Patch(StatusCode, '/users/42', '',    '{"role": "admin"}');  try    if StatusCode = 200 then      ShowMessage('User updated')    else      ShowMessage('Failed: ' + StatusCode.ToString);  finally    Response.Free;  end;end;

PUT (Full Replace)#

pascal
var  StatusCode: Integer;  Response: TJSONObject;begin  Response := Tina4REST1.Put(StatusCode, '/users/42', '',    '{"name": "Alice", "email": "alice@new.com", "role": "admin"}');  try    // handle response  finally    Response.Free;  end;end;

DELETE#

pascal
var  StatusCode: Integer;  Response: TJSONObject;begin  Response := Tina4REST1.Delete(StatusCode, '/users/42');  try    if StatusCode = 204 then      ShowMessage('Deleted')    else      ShowMessage('Failed: ' + StatusCode.ToString);  finally    Response.Free;  end;end;

Method Reference#

MethodSignatureHTTP Verb
GetGet(var StatusCode: Integer; EndPoint: string; QueryParams: string = ''): TJSONObjectGET
PostPost(var StatusCode: Integer; EndPoint: string; QueryParams: string = ''; Body: string = ''): TJSONObjectPOST
PatchPatch(var StatusCode: Integer; EndPoint: string; QueryParams: string = ''; Body: string = ''): TJSONObjectPATCH
PutPut(var StatusCode: Integer; EndPoint: string; QueryParams: string = ''; Body: string = ''): TJSONObjectPUT
DeleteDelete(var StatusCode: Integer; EndPoint: string; QueryParams: string = ''): TJSONObjectDELETE

3. Authentication Patterns#

Basic Auth#

Set Username and Password on TTina4REST. Every request includes an Authorization: Basic header automatically:

pascal
Tina4REST1.BaseUrl := 'https://api.example.com';Tina4REST1.Username := 'apiuser';Tina4REST1.Password := 'apipassword';

Bearer Token (Static)#

If you have a long-lived API key or token:

pascal
Tina4REST1.BaseUrl := 'https://api.example.com';Tina4REST1.SetBearer('your-api-key-here');

Bearer Token (Login Flow)#

Most apps require a login step that returns a short-lived token:

pascal
procedure TForm1.Login;var  StatusCode: Integer;  Response: TJSONObject;begin  // Use a temporary REST component with no auth for the login call  Tina4REST1.BaseUrl := 'https://api.example.com';โ€‹  Response := Tina4REST1.Post(StatusCode, '/auth/login', '',    Format('{"email": "%s", "password": "%s"}',      [edtEmail.Text, edtPassword.Text]));  try    if StatusCode = 200 then    begin      var Token := Response.GetValue<String>('token');      Tina4REST1.SetBearer(Token);      ShowMessage('Logged in successfully');    end    else      ShowMessage('Login failed: ' + Response.GetValue<String>('message'));  finally    Response.Free;  end;end;

Custom Headers#

For APIs that require custom headers (API keys in headers, tenant IDs, etc.), use SendHttpRequest from Tina4Core directly:

pascal
uses Tina4Core;โ€‹var  StatusCode: Integer;  Headers: TNetHeaders;  Response: TBytes;begin  SetLength(Headers, 2);  Headers[0] := TNameValuePair.Create('X-API-Key', 'my-key-123');  Headers[1] := TNameValuePair.Create('X-Tenant-Id', 'acme-corp');โ€‹  Response := SendHttpRequest(StatusCode,    'https://api.example.com', '/data', '', '',    'application/json', 'utf-8', '', '', Headers);end;

4. TTina4RESTRequest -- Declarative REST#

Direct calls give you control. TTina4RESTRequest gives you convenience. Link it to a TTina4REST, set properties, and execute. The component handles the HTTP call, parses the JSON response, creates MemTable field definitions, and populates the table. One method call.

Basic GET with Auto MemTable Population#

Drop these on your form:

  • TTina4REST (name: Tina4REST1)
  • TTina4RESTRequest (name: Tina4RESTRequest1)
  • TFDMemTable (name: FDMemTable1)

Configure Tina4RESTRequest1:

PropertyValue
Tina4RESTTina4REST1
EndPoint/users
RequestTypeGet
DataKeyrecords
MemTableFDMemTable1
SyncModeClear

Execute:

pascal
Tina4RESTRequest1.ExecuteRESTCall;// FDMemTable1 now contains all users from the "records" array

The DataKey tells the component which JSON key contains the array of records. If your API returns {"records": [...]}, set DataKey to records. If the response is a bare JSON array [...], leave DataKey empty.

POST with RequestBody#

pascal
Tina4RESTRequest1.RequestType := TTina4RequestType.Post;Tina4RESTRequest1.EndPoint := '/users';Tina4RESTRequest1.RequestBody.Text :=  '{"name": "Alice", "email": "alice@example.com", "role": "editor"}';Tina4RESTRequest1.ExecuteRESTCall;

The RequestBody is a TStringList. Set it with .Text for single-line JSON, or use .Add for multiline construction.

PUT / PATCH / DELETE#

Change the RequestType property:

pascal
// UpdateTina4RESTRequest1.RequestType := TTina4RequestType.Put;Tina4RESTRequest1.EndPoint := '/users/42';Tina4RESTRequest1.RequestBody.Text := '{"name": "Alice Updated"}';Tina4RESTRequest1.ExecuteRESTCall;โ€‹// DeleteTina4RESTRequest1.RequestType := TTina4RequestType.Delete;Tina4RESTRequest1.EndPoint := '/users/42';Tina4RESTRequest1.ExecuteRESTCall;

5. Master/Detail with Parameter Injection#

This is where TTina4RESTRequest earns its keep. Set a MasterSource and the detail request injects field values from the master's MemTable into the endpoint, request body, and query params using {fieldName} placeholders.

Setup#

pascal
// Master: fetches all customersTina4RESTRequest1.Tina4REST := Tina4REST1;Tina4RESTRequest1.EndPoint := '/customers';Tina4RESTRequest1.DataKey := 'records';Tina4RESTRequest1.MemTable := FDMemTableCustomers;Tina4RESTRequest1.RequestType := TTina4RequestType.Get;โ€‹// Detail: fetches orders for the selected customerTina4RESTRequest2.Tina4REST := Tina4REST1;Tina4RESTRequest2.MasterSource := Tina4RESTRequest1;Tina4RESTRequest2.EndPoint := '/customers/{id}/orders';Tina4RESTRequest2.DataKey := 'records';Tina4RESTRequest2.MemTable := FDMemTableOrders;Tina4RESTRequest2.RequestType := TTina4RequestType.Get;

When the master executes and the user navigates to a customer with id = 5, the detail's endpoint becomes /customers/5/orders. The {id} placeholder is replaced with the current value of the id field from FDMemTableCustomers.

How It Works#

  1. The master request executes and populates FDMemTableCustomers.
  2. When you scroll to a different row in FDMemTableCustomers, the detail request re-executes automatically.
  3. The detail request scans its EndPoint, RequestBody, and QueryParams for {fieldName} patterns.
  4. Each pattern is replaced with the current field value from the master's MemTable.

Multiple Placeholders#

You can use multiple placeholders:

pascal
Tina4RESTRequest2.EndPoint := '/customers/{customerId}/orders';Tina4RESTRequest2.RequestBody.Text :=  '{"customerId": "{customerId}", "status": "active"}';

6. POST from SourceMemTable#

Sometimes you need to send data that already exists in a MemTable -- an import batch, a modified dataset, user edits. Instead of manually serializing rows to JSON, link a SourceMemTable:

pascal
Tina4RESTRequest1.RequestType := TTina4RequestType.Post;Tina4RESTRequest1.EndPoint := '/import/products';Tina4RESTRequest1.SourceMemTable := FDMemTableProducts;Tina4RESTRequest1.SourceIgnoreFields := 'internal_id,temp_flag';Tina4RESTRequest1.SourceIgnoreBlanks := True;Tina4RESTRequest1.ExecuteRESTCall;

The component serializes all rows from FDMemTableProducts to a JSON array and sends it as the POST body. Fields listed in SourceIgnoreFields are excluded. If SourceIgnoreBlanks is True, fields with empty values are omitted from each row.


7. Async Execution#

REST calls block the main thread. For a quick local API, that is fine. For a slow endpoint or a large response, your UI freezes. ExecuteRESTCallAsync runs the request in a background thread.

pascal
procedure TForm1.FormCreate(Sender: TObject);begin  Tina4RESTRequest1.OnExecuteDone := HandleRequestDone;end;โ€‹procedure TForm1.HandleRequestDone(Sender: TObject);begin  TThread.Synchronize(nil, procedure  begin    ShowMessage('Loaded ' + FDMemTable1.RecordCount.ToString + ' records');    // Update your grid or UI here -- you are now on the main thread  end);end;โ€‹procedure TForm1.btnFetchClick(Sender: TObject);begin  btnFetch.Enabled := False;  Tina4RESTRequest1.ExecuteRESTCallAsync;end;

Thread Safety Rules#

  1. Never access UI controls from the background thread. The OnExecuteDone event fires on the background thread. Wrap all UI updates in TThread.Synchronize.
  2. The MemTable is populated before OnExecuteDone fires. You can read the MemTable inside the synchronized block.
  3. Disable buttons while the request is in flight. Re-enable them in OnExecuteDone.

8. Events#

OnExecuteDone#

Fires after the REST call completes and the MemTable is populated (if configured). Use it for post-processing, UI updates, or chaining requests:

pascal
procedure TForm1.Request1ExecuteDone(Sender: TObject);begin  TThread.Synchronize(nil, procedure  begin    lblCount.Text := Format('%d records loaded', [FDMemTable1.RecordCount]);โ€‹    // Chain: now fetch details for the first record    if FDMemTable1.RecordCount > 0 then    begin      FDMemTable1.First;      Tina4RESTRequest2.ExecuteRESTCall;    end;  end);end;

OnAddRecord#

Fires for each record added to the MemTable during population. Use it for custom field transformations, filtering, or logging:

pascal
procedure TForm1.Request1AddRecord(Sender: TObject);begin  // Access the MemTable -- the cursor is on the newly added record  var Status := FDMemTable1.FieldByName('status').AsString;  if Status = 'inactive' then    FDMemTable1.Delete;  // Remove inactive records during importend;

9. Complete Example: Customer Management Panel#

A real-world scenario. List customers. View details. Create new ones. Update existing ones. Four operations, four REST calls, one form.

Form Design#

  • TTina4REST (name: restAPI, BaseUrl: https://api.example.com/v1)
  • TTina4RESTRequest (name: reqListCustomers)
  • TTina4RESTRequest (name: reqCreateCustomer)
  • TTina4RESTRequest (name: reqUpdateCustomer)
  • TFDMemTable (name: mtCustomers)
  • TStringGrid (name: gridCustomers)
  • TEdit (name: edtName)
  • TEdit (name: edtEmail)
  • TButton (name: btnLoad, Text: Load)
  • TButton (name: btnSave, Text: Save)
  • TLabel (name: lblStatus)

Implementation#

pascal
unit CustomerPanel;โ€‹interfaceโ€‹uses  System.SysUtils, System.Classes, System.JSON,  FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls, FMX.Edit,  FMX.Grid, FMX.Grid.Style, FMX.ScrollBox, FMX.Layouts,  FireDAC.Comp.Client,  Tina4REST, Tina4RESTRequest;โ€‹type  TfrmCustomers = class(TForm)    restAPI: TTina4REST;    reqListCustomers: TTina4RESTRequest;    reqCreateCustomer: TTina4RESTRequest;    reqUpdateCustomer: TTina4RESTRequest;    mtCustomers: TFDMemTable;    gridCustomers: TStringGrid;    edtName: TEdit;    edtEmail: TEdit;    btnLoad: TButton;    btnSave: TButton;    lblStatus: TLabel;    procedure FormCreate(Sender: TObject);    procedure btnLoadClick(Sender: TObject);    procedure btnSaveClick(Sender: TObject);    procedure gridCustomersSelectCell(Sender: TObject; const ACol, ARow: Integer;      var CanSelect: Boolean);  private    FSelectedId: string;    procedure SetupRequests;    procedure RefreshGrid;    procedure SetStatus(const Msg: string);  end;โ€‹var  frmCustomers: TfrmCustomers;โ€‹implementationโ€‹{$R *.fmx}โ€‹procedure TfrmCustomers.FormCreate(Sender: TObject);begin  restAPI.BaseUrl := 'https://api.example.com/v1';  restAPI.SetBearer('your-token-here');  FSelectedId := '';  SetupRequests;end;โ€‹procedure TfrmCustomers.SetupRequests;begin  // List customers  reqListCustomers.Tina4REST := restAPI;  reqListCustomers.EndPoint := '/customers';  reqListCustomers.RequestType := TTina4RequestType.Get;  reqListCustomers.DataKey := 'records';  reqListCustomers.MemTable := mtCustomers;  reqListCustomers.SyncMode := TTina4RestSyncMode.Clear;โ€‹  // Create customer  reqCreateCustomer.Tina4REST := restAPI;  reqCreateCustomer.EndPoint := '/customers';  reqCreateCustomer.RequestType := TTina4RequestType.Post;โ€‹  // Update customer  reqUpdateCustomer.Tina4REST := restAPI;  reqUpdateCustomer.RequestType := TTina4RequestType.Put;end;โ€‹procedure TfrmCustomers.btnLoadClick(Sender: TObject);begin  reqListCustomers.ExecuteRESTCall;  RefreshGrid;  SetStatus(Format('Loaded %d customers', [mtCustomers.RecordCount]));end;โ€‹procedure TfrmCustomers.btnSaveClick(Sender: TObject);var  Body: string;  StatusCode: Integer;  Response: TJSONObject;begin  Body := Format('{"name": "%s", "email": "%s"}',    [edtName.Text, edtEmail.Text]);โ€‹  if FSelectedId <> '' then  begin    // Update existing customer    Response := restAPI.Put(StatusCode,      '/customers/' + FSelectedId, '', Body);    try      if StatusCode = 200 then        SetStatus('Customer updated')      else        SetStatus('Update failed: ' + StatusCode.ToString);    finally      Response.Free;    end;  end  else  begin    // Create new customer    Response := restAPI.Post(StatusCode, '/customers', '', Body);    try      if StatusCode = 201 then        SetStatus('Customer created')      else        SetStatus('Create failed: ' + StatusCode.ToString);    finally      Response.Free;    end;  end;โ€‹  // Refresh the list  FSelectedId := '';  edtName.Text := '';  edtEmail.Text := '';  btnLoadClick(nil);end;โ€‹procedure TfrmCustomers.gridCustomersSelectCell(Sender: TObject;  const ACol, ARow: Integer; var CanSelect: Boolean);begin  if ARow < mtCustomers.RecordCount then  begin    mtCustomers.RecNo := ARow + 1;    FSelectedId := mtCustomers.FieldByName('id').AsString;    edtName.Text := mtCustomers.FieldByName('name').AsString;    edtEmail.Text := mtCustomers.FieldByName('email').AsString;  end;end;โ€‹procedure TfrmCustomers.RefreshGrid;begin  gridCustomers.RowCount := mtCustomers.RecordCount;  gridCustomers.ClearColumns;โ€‹  var ColId := TStringColumn.Create(gridCustomers);  ColId.Header := 'ID';  ColId.Width := 50;  gridCustomers.AddObject(ColId);โ€‹  var ColName := TStringColumn.Create(gridCustomers);  ColName.Header := 'Name';  ColName.Width := 200;  gridCustomers.AddObject(ColName);โ€‹  var ColEmail := TStringColumn.Create(gridCustomers);  ColEmail.Header := 'Email';  ColEmail.Width := 250;  gridCustomers.AddObject(ColEmail);โ€‹  mtCustomers.First;  var Row := 0;  while not mtCustomers.Eof do  begin    gridCustomers.Cells[0, Row] := mtCustomers.FieldByName('id').AsString;    gridCustomers.Cells[1, Row] := mtCustomers.FieldByName('name').AsString;    gridCustomers.Cells[2, Row] := mtCustomers.FieldByName('email').AsString;    Inc(Row);    mtCustomers.Next;  end;end;โ€‹procedure TfrmCustomers.SetStatus(const Msg: string);begin  lblStatus.Text := Msg;end;โ€‹end.

10. Exercise: Product Catalog#

Build a product management application with the following features:

Requirements#

  1. Fetch products from GET /products (use jsonplaceholder or your own API)
  2. Display products in a TStringGrid
  3. Add a search TEdit that filters products by title (client-side filtering on the MemTable)
  4. Add a form to create new products via POST /products
  5. Use async execution for the initial load with a loading indicator

Solution#

pascal
unit ProductCatalog;โ€‹interfaceโ€‹uses  System.SysUtils, System.Classes, System.JSON,  FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls, FMX.Edit,  FMX.Grid, FMX.Grid.Style, FMX.ScrollBox, FMX.Layouts,  FireDAC.Comp.Client,  Tina4REST, Tina4RESTRequest;โ€‹type  TfrmProducts = class(TForm)    restAPI: TTina4REST;    reqProducts: TTina4RESTRequest;    mtProducts: TFDMemTable;    gridProducts: TStringGrid;    edtSearch: TEdit;    edtTitle: TEdit;    edtPrice: TEdit;    btnCreate: TButton;    lblLoading: TLabel;    procedure FormCreate(Sender: TObject);    procedure edtSearchChangeTracking(Sender: TObject);    procedure btnCreateClick(Sender: TObject);  private    procedure OnProductsLoaded(Sender: TObject);    procedure RefreshGrid;    procedure FilterGrid(const SearchText: string);  end;โ€‹var  frmProducts: TfrmProducts;โ€‹implementationโ€‹{$R *.fmx}โ€‹procedure TfrmProducts.FormCreate(Sender: TObject);begin  restAPI.BaseUrl := 'https://jsonplaceholder.typicode.com';โ€‹  reqProducts.Tina4REST := restAPI;  reqProducts.EndPoint := '/posts'; // Using posts as stand-in for products  reqProducts.RequestType := TTina4RequestType.Get;  reqProducts.MemTable := mtProducts;  reqProducts.SyncMode := TTina4RestSyncMode.Clear;โ€‹  reqProducts.OnExecuteDone := OnProductsLoaded;โ€‹  lblLoading.Text := 'Loading products...';  lblLoading.Visible := True;  reqProducts.ExecuteRESTCallAsync;end;โ€‹procedure TfrmProducts.OnProductsLoaded(Sender: TObject);begin  TThread.Synchronize(nil, procedure  begin    lblLoading.Visible := False;    RefreshGrid;  end);end;โ€‹procedure TfrmProducts.RefreshGrid;begin  gridProducts.RowCount := mtProducts.RecordCount;  gridProducts.ClearColumns;โ€‹  for var I := 0 to mtProducts.FieldCount - 1 do  begin    var Col := TStringColumn.Create(gridProducts);    Col.Header := mtProducts.Fields[I].FieldName;    Col.Width := 150;    gridProducts.AddObject(Col);  end;โ€‹  mtProducts.First;  var Row := 0;  while not mtProducts.Eof do  begin    for var C := 0 to mtProducts.FieldCount - 1 do      gridProducts.Cells[C, Row] := mtProducts.Fields[C].AsString;    Inc(Row);    mtProducts.Next;  end;end;โ€‹procedure TfrmProducts.edtSearchChangeTracking(Sender: TObject);begin  FilterGrid(edtSearch.Text);end;โ€‹procedure TfrmProducts.FilterGrid(const SearchText: string);var  Row: Integer;begin  if SearchText = '' then  begin    RefreshGrid;    Exit;  end;โ€‹  Row := 0;  gridProducts.RowCount := 0;โ€‹  mtProducts.First;  while not mtProducts.Eof do  begin    var Title := mtProducts.FieldByName('title').AsString;    if Title.ToLower.Contains(SearchText.ToLower) then    begin      gridProducts.RowCount := Row + 1;      for var C := 0 to mtProducts.FieldCount - 1 do        gridProducts.Cells[C, Row] := mtProducts.Fields[C].AsString;      Inc(Row);    end;    mtProducts.Next;  end;end;โ€‹procedure TfrmProducts.btnCreateClick(Sender: TObject);var  StatusCode: Integer;  Response: TJSONObject;begin  Response := restAPI.Post(StatusCode, '/posts', '',    Format('{"title": "%s", "body": "%s", "userId": 1}',      [edtTitle.Text, edtPrice.Text]));  try    if StatusCode = 201 then    begin      ShowMessage('Product created with ID: ' + Response.GetValue<String>('id'));      edtTitle.Text := '';      edtPrice.Text := '';    end    else      ShowMessage('Failed: ' + StatusCode.ToString);  finally    Response.Free;  end;end;โ€‹end.

11. Common Gotchas#

Forgetting to Free TJSONObject#

Symptom: Memory usage grows over time. ReportMemoryLeaksOnShutdown shows leaks.

Fix: Every Get, Post, Patch, Put, and Delete call returns a TJSONObject that you own. Always wrap in try..finally:

pascal
var Response := Tina4REST1.Get(StatusCode, '/data');try  // use Responsefinally  Response.Free;  // Always. Every time.end;

Not Checking StatusCode#

Symptom: Application crashes when trying to read fields from an error response.

Fix: Always check the status code before accessing response data:

pascal
Response := Tina4REST1.Get(StatusCode, '/users/999');try  if StatusCode = 200 then    ProcessUser(Response)  else if StatusCode = 404 then    ShowMessage('User not found')  else    ShowMessage('Unexpected error: ' + StatusCode.ToString);finally  Response.Free;end;

Async Thread Safety#

Symptom: Intermittent access violations, garbled UI, or "Canvas does not allow drawing" errors.

Fix: Never touch UI controls from OnExecuteDone without TThread.Synchronize:

pascal
// WRONG -- will crash randomlyprocedure TForm1.OnDone(Sender: TObject);begin  lblStatus.Text := 'Done';  // Main thread violationend;โ€‹// CORRECTprocedure TForm1.OnDone(Sender: TObject);begin  TThread.Synchronize(nil, procedure  begin    lblStatus.Text := 'Done';  // Safe -- runs on main thread  end);end;

DataKey Mismatch#

Symptom: MemTable is empty after a successful request.

Fix: Check that DataKey matches the JSON structure. If the API returns {"data": [...]}, set DataKey to data. If it returns {"results": [...]}, set it to results. If the response is a bare array [...], leave DataKey empty.

BaseUrl Trailing Slash#

Symptom: 404 errors on endpoints that work in the browser.

Fix: Do not include a trailing slash on BaseUrl. The endpoint already starts with /:

pascal
// WRONGTina4REST1.BaseUrl := 'https://api.example.com/v1/';// Endpoint '/users' becomes 'https://api.example.com/v1//users'โ€‹// CORRECTTina4REST1.BaseUrl := 'https://api.example.com/v1';

Summary#

WhatHow
Base configurationTTina4REST -- set BaseUrl, auth
Basic AuthSet Username and Password
Bearer tokenSetBearer('token')
Direct GETTina4REST1.Get(StatusCode, '/endpoint', 'params')
Direct POSTTina4REST1.Post(StatusCode, '/endpoint', '', Body)
Direct PATCHTina4REST1.Patch(StatusCode, '/endpoint', '', Body)
Direct PUTTina4REST1.Put(StatusCode, '/endpoint', '', Body)
Direct DELETETina4REST1.Delete(StatusCode, '/endpoint')
Declarative GETTTina4RESTRequest -- set endpoint, MemTable, ExecuteRESTCall
Master/DetailSet MasterSource, use {fieldName} placeholders
POST from MemTableSet SourceMemTable, call ExecuteRESTCall
AsyncExecuteRESTCallAsync + OnExecuteDone + TThread.Synchronize
Memory ruleEvery TJSONObject returned must be freed by the caller