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
- Serves a static HTML homepage at
GET / - Returns the server's current time at
GET /api/time - 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 - Returns a 404 JSON error for unknown routes
- 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
-
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.
-
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).
-
JSON is the API language. API endpoints accept and return JSON. The fpjson library makes this natural in Pascal.
-
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. -
Concurrency is the next frontier. MicroServe handles one request at a time. For real traffic, it needs threads — the subject of Chapter 36.