Case Study 2: MicroServe: A Complete HTTP Server in Pascal

Overview

This case study assembles all the pieces from Chapter 35 into a complete, working HTTP server. MicroServe serves HTML pages, handles multiple routes, returns JSON API responses, and includes basic error handling. While not production-grade, it demonstrates every component of an HTTP server and could genuinely serve as the backend for a small application.


What MicroServe Does

  1. Serves a static HTML homepage at GET /
  2. Returns the server's current time at GET /api/time
  3. Manages an in-memory list of expenses: - GET /api/expenses — list all expenses as JSON - POST /api/expenses — add a new expense from JSON body
  4. Returns a 404 JSON error for unknown routes
  5. Logs every request to the console

The Complete Server

program MicroServe;

{$mode objfpc}{$H+}

uses
  SysUtils, Classes, ssockets, fpjson;

type
  THTTPRequest = record
    Method: string;
    Path: string;
    Headers: TStringList;
    Body: string;
  end;

  TExpenseEntry = record
    ID: Integer;
    Description: string;
    Amount: Double;
    Category: string;
  end;

var
  Expenses: array of TExpenseEntry;
  NextID: Integer = 1;

{ === Request Parser === }

function ParseRequest(const Raw: string): THTTPRequest;
var
  Lines: TStringList;
  Parts: array of string;
  I, BlankLine: Integer;
begin
  Result.Headers := TStringList.Create;
  Lines := TStringList.Create;
  try
    Lines.Text := Raw;
    if Lines.Count = 0 then Exit;

    { Request line }
    SetLength(Parts, 0);
    Parts := Lines[0].Split([' ']);
    if Length(Parts) >= 2 then
    begin
      Result.Method := Parts[0];
      Result.Path := Parts[1];
    end;

    { Headers }
    BlankLine := Lines.Count;
    for I := 1 to Lines.Count - 1 do
    begin
      if Trim(Lines[I]) = '' then
      begin
        BlankLine := I;
        Break;
      end;
      Result.Headers.Add(Lines[I]);
    end;

    { Body }
    Result.Body := '';
    for I := BlankLine + 1 to Lines.Count - 1 do
      Result.Body := Result.Body + Lines[I];
  finally
    Lines.Free;
  end;
end;

{ === Response Formatting === }

function HTTPResponse(Code: Integer; const Status, ContentType, Body: string): string;
begin
  Result := Format('HTTP/1.1 %d %s'#13#10, [Code, Status]);
  Result := Result + 'Content-Type: ' + ContentType + #13#10;
  Result := Result + 'Content-Length: ' + IntToStr(Length(Body)) + #13#10;
  Result := Result + 'Connection: close' + #13#10;
  Result := Result + 'Server: MicroServe/1.0 (FreePascal)'#13#10;
  Result := Result + 'Access-Control-Allow-Origin: *'#13#10;
  Result := Result + #13#10;
  Result := Result + Body;
end;

{ === Route Handlers === }

function HandleHomePage: string;
var
  HTML: string;
begin
  HTML :=
    '<!DOCTYPE html>'#10 +
    '<html><head><title>MicroServe</title>'#10 +
    '<style>'#10 +
    '  body { font-family: sans-serif; max-width: 700px; margin: 40px auto; }'#10 +
    '  h1 { color: #2c3e50; }'#10 +
    '  code { background: #ecf0f1; padding: 2px 6px; border-radius: 3px; }'#10 +
    '  .endpoint { margin: 10px 0; padding: 10px; background: #f8f9fa; border-left: 3px solid #3498db; }'#10 +
    '</style></head><body>'#10 +
    '<h1>MicroServe</h1>'#10 +
    '<p>A minimal HTTP server written in Free Pascal.</p>'#10 +
    '<h2>API Endpoints</h2>'#10 +
    '<div class="endpoint"><code>GET /api/time</code> — Current server time</div>'#10 +
    '<div class="endpoint"><code>GET /api/expenses</code> — List all expenses</div>'#10 +
    '<div class="endpoint"><code>POST /api/expenses</code> — Add an expense (JSON body)</div>'#10 +
    '<p>Running on Free Pascal ' + {$I %FPCVERSION%} + '</p>'#10 +
    '</body></html>';
  Result := HTTPResponse(200, 'OK', 'text/html; charset=utf-8', HTML);
end;

function HandleGetTime: string;
var
  Obj: TJSONObject;
begin
  Obj := TJSONObject.Create;
  try
    Obj.Add('time', FormatDateTime('hh:nn:ss', Now));
    Obj.Add('date', FormatDateTime('yyyy-mm-dd', Now));
    Obj.Add('timestamp', DateTimeToStr(Now));
    Result := HTTPResponse(200, 'OK', 'application/json', Obj.FormatJSON);
  finally
    Obj.Free;
  end;
end;

function HandleGetExpenses: string;
var
  Arr: TJSONArray;
  Obj: TJSONObject;
  I: Integer;
begin
  Arr := TJSONArray.Create;
  try
    for I := 0 to High(Expenses) do
    begin
      Obj := TJSONObject.Create;
      Obj.Add('id', Expenses[I].ID);
      Obj.Add('description', Expenses[I].Description);
      Obj.Add('amount', Expenses[I].Amount);
      Obj.Add('category', Expenses[I].Category);
      Arr.Add(Obj);
    end;
    Result := HTTPResponse(200, 'OK', 'application/json', Arr.FormatJSON);
  finally
    Arr.Free;
  end;
end;

function HandlePostExpense(const Body: string): string;
var
  Data: TJSONData;
  Obj, ResponseObj: TJSONObject;
  NewExpense: TExpenseEntry;
begin
  if Trim(Body) = '' then
  begin
    Result := HTTPResponse(400, 'Bad Request', 'application/json',
      '{"error":"Empty request body"}');
    Exit;
  end;

  try
    Data := GetJSON(Body);
  except
    Result := HTTPResponse(400, 'Bad Request', 'application/json',
      '{"error":"Invalid JSON"}');
    Exit;
  end;

  try
    if not (Data is TJSONObject) then
    begin
      Result := HTTPResponse(400, 'Bad Request', 'application/json',
        '{"error":"Expected JSON object"}');
      Exit;
    end;

    Obj := Data as TJSONObject;
    NewExpense.ID := NextID;
    Inc(NextID);
    NewExpense.Description := Obj.Get('description', '');
    NewExpense.Amount := Obj.Get('amount', 0.0);
    NewExpense.Category := Obj.Get('category', 'other');

    if NewExpense.Description = '' then
    begin
      Result := HTTPResponse(400, 'Bad Request', 'application/json',
        '{"error":"description is required"}');
      Exit;
    end;

    SetLength(Expenses, Length(Expenses) + 1);
    Expenses[High(Expenses)] := NewExpense;

    ResponseObj := TJSONObject.Create;
    try
      ResponseObj.Add('id', NewExpense.ID);
      ResponseObj.Add('description', NewExpense.Description);
      ResponseObj.Add('amount', NewExpense.Amount);
      ResponseObj.Add('category', NewExpense.Category);
      ResponseObj.Add('message', 'Expense created');
      Result := HTTPResponse(201, 'Created', 'application/json',
        ResponseObj.FormatJSON);
    finally
      ResponseObj.Free;
    end;
  finally
    Data.Free;
  end;
end;

function HandleNotFound(const Path: string): string;
begin
  Result := HTTPResponse(404, 'Not Found', 'application/json',
    '{"error":"Not found","path":"' + Path + '"}');
end;

{ === Main Server Loop === }

type
  TMicroServer = class(TInetServer)
  public
    procedure OnClientConnect(Sender: TObject; Data: TSocketStream);
  end;

procedure TMicroServer.OnClientConnect(Sender: TObject; Data: TSocketStream);
var
  Buffer: array[0..8191] of Byte;
  BytesRead: Integer;
  RawRequest, Response: string;
  Request: THTTPRequest;
begin
  BytesRead := Data.Read(Buffer, SizeOf(Buffer));
  if BytesRead <= 0 then Exit;

  SetString(RawRequest, PChar(@Buffer[0]), BytesRead);
  Request := ParseRequest(RawRequest);
  try
    { Log the request }
    WriteLn(FormatDateTime('hh:nn:ss', Now), ' ',
            Request.Method, ' ', Request.Path);

    { Route to handler }
    if (Request.Method = 'GET') and (Request.Path = '/') then
      Response := HandleHomePage
    else if (Request.Method = 'GET') and (Request.Path = '/api/time') then
      Response := HandleGetTime
    else if (Request.Method = 'GET') and (Request.Path = '/api/expenses') then
      Response := HandleGetExpenses
    else if (Request.Method = 'POST') and (Request.Path = '/api/expenses') then
      Response := HandlePostExpense(Request.Body)
    else
      Response := HandleNotFound(Request.Path);

    Data.Write(Response[1], Length(Response));
  finally
    Request.Headers.Free;
  end;
end;

var
  Server: TMicroServer;
  Port: Word;
begin
  Port := 8080;
  if ParamCount >= 1 then
    Port := StrToIntDef(ParamStr(1), 8080);

  WriteLn('MicroServe v1.0 — A Pascal HTTP Server');
  WriteLn('Listening on http://localhost:', Port, '/');
  WriteLn('Press Ctrl+C to stop.');
  WriteLn;

  { Seed with sample data }
  SetLength(Expenses, 2);
  Expenses[0].ID := 1; Inc(NextID);
  Expenses[0].Description := 'Groceries';
  Expenses[0].Amount := 85.50;
  Expenses[0].Category := 'food';
  Expenses[1].ID := 2; Inc(NextID);
  Expenses[1].Description := 'Bus pass';
  Expenses[1].Amount := 45.00;
  Expenses[1].Category := 'transport';

  Server := TMicroServer.Create('0.0.0.0', Port);
  try
    Server.OnConnect := @Server.OnClientConnect;
    Server.StartAccepting;
  finally
    Server.Free;
  end;
end.

Testing MicroServe

Open a web browser and navigate to http://localhost:8080/. You should see the HTML homepage.

Test the API with curl:

# Get current time
curl http://localhost:8080/api/time

# List expenses
curl http://localhost:8080/api/expenses

# Add an expense
curl -X POST http://localhost:8080/api/expenses \
  -H "Content-Type: application/json" \
  -d '{"description":"Coffee","amount":4.50,"category":"food"}'

# List again to see the new expense
curl http://localhost:8080/api/expenses

What MicroServe Does NOT Do (Yet)

Feature Status Solution
Handle concurrent requests No — blocking Threads (Chapter 36)
HTTPS/TLS No — plain HTTP only OpenSSL library or reverse proxy
Persistent storage No — in-memory only Database (Chapter 31) or file (Chapter 34)
Authentication No — open access Token-based auth header checking
Static file serving No — routes only File I/O based on path
URL parameters Partial — no query parsing String parsing of path

These limitations are deliberate — MicroServe is a teaching tool, not a production server. Each limitation is a learning opportunity.


Lessons Learned

  1. HTTP is text. Despite its complexity as a standard, HTTP at its core is a text protocol with a simple structure. Understanding this demystifies everything built on top of it.

  2. Routing is pattern matching. The router compares the request method and path to a table of registered handlers. Every web framework does this — just with more features (wildcards, parameters, middleware).

  3. JSON is the API language. API endpoints accept and return JSON. The fpjson library makes this natural in Pascal.

  4. Error responses matter. Returning {"error":"description is required"} with a 400 status code is far more helpful than a generic 500 error or a crash.

  5. Concurrency is the next frontier. MicroServe handles one request at a time. For real traffic, it needs threads — the subject of Chapter 36.