Tutorial: Building a Web App
HarbourBuilder includes a built-in TWebServer component that lets you serve web pages and JSON APIs directly from your application. In this tutorial you will build a simple TODO web app with both a desktop GUI and a browser-based interface running side by side.
Step 1: Create the Project
- Create a new project called
TodoWebAppvia File → New Project. - Open
main.prgin the Code Editor. - We will build a desktop form for managing todos and a web server that exposes the same data.
Step 2: Set Up the Web Server
Drop a TWebServer from the Internet palette tab onto the form, or create it in code. Set the port and define your routes.
#include "hbbuilder.ch" static aTodos := {} // Shared todo list function Main() local oForm, oServer, oBrw, oGet, oBtnAdd local cTask := "" // --- Web server setup --- DEFINE WEBSERVER oServer PORT 8080 oServer:Route( "GET", "/", { |oReq, oRes| ServeHomePage( oReq, oRes ) } ) oServer:Route( "GET", "/api/todos", { |oReq, oRes| GetTodosJSON( oReq, oRes ) } ) oServer:Route( "POST", "/api/todos", { |oReq, oRes| AddTodoJSON( oReq, oRes ) } ) oServer:Route( "DELETE", "/api/todos/:id", { |oReq, oRes| DeleteTodoJSON( oReq, oRes ) } ) oServer:Start() // --- Desktop UI --- DEFINE FORM oForm TITLE "TODO App (Desktop + Web on port 8080)" ; SIZE 600, 500 FONT "Segoe UI", 10 @ 10, 10 BROWSE oBrw ; OF oForm SIZE 560, 350 ; HEADERS { "#", "Task", "Status" } ; WIDTHS { 40, 350, 100 } @ 380, 10 GET oGet VAR cTask OF oForm SIZE 400, 24 @ 380, 420 BUTTON oBtnAdd PROMPT "Add Task" ; OF oForm SIZE 100, 32 ; ACTION ( AAdd( aTodos, { oGet:GetValue(), "pending" } ), ; RefreshGrid( oBrw ), oGet:SetValue( "" ) ) RefreshGrid( oBrw ) oForm:OnClose := { || oServer:Stop(), .T. } ACTIVATE FORM oForm CENTERED return nil
The TWebServer runs on a background thread, so your desktop form remains fully interactive.
Both the GUI and the browser see the same aTodos array. Changes from either side
are immediately visible to the other.
Step 3: Serve an HTML Page
The root route / serves a complete HTML page. Use oRes:SendHTML() to return HTML content.
static function ServeHomePage( oReq, oRes ) local cHTML := '<!DOCTYPE html>' + ; '<html><head><title>TODO App</title>' + ; '<style>body{font-family:sans-serif;max-width:600px;margin:2em auto}' + ; 'input{padding:8px;width:70%} button{padding:8px 16px}' + ; 'li{padding:4px 0}</style></head><body>' + ; '<h1>TODO List</h1>' + ; '<input id="task" placeholder="New task...">' + ; '<button onclick="addTask()">Add</button>' + ; '<ul id="list"></ul>' + ; '<script>' + ; 'async function load(){let r=await fetch("/api/todos");' + ; 'let d=await r.json();let h="";' + ; 'd.forEach((t,i)=>h+="<li>"+t[0]+" ["+t[1]+"] " + ; '+"<a href=# onclick=del("+i+")>x</a></li>");' + ; 'document.getElementById("list").innerHTML=h}' + ; 'async function addTask(){let v=document.getElementById("task").value;' + ; 'await fetch("/api/todos",{method:"POST",' + ; 'headers:{"Content-Type":"application/json"},' + ; 'body:JSON.stringify({task:v})});load()}' + ; 'async function del(i){await fetch("/api/todos/"+i,' + ; '{method:"DELETE"});load()}' + ; 'load();</script></body></html>' oRes:SendHTML( cHTML ) return nil
Step 4: Return JSON for the API
The API routes use oRes:SendJSON() to return structured data that the browser JavaScript
(or any HTTP client) can consume.
static function GetTodosJSON( oReq, oRes ) oRes:SendJSON( hb_jsonEncode( aTodos ) ) return nil static function AddTodoJSON( oReq, oRes ) local hBody := hb_jsonDecode( oReq:cBody ) AAdd( aTodos, { hBody[ "task" ], "pending" } ) oRes:SendJSON( '{"status":"ok"}' ) return nil static function DeleteTodoJSON( oReq, oRes ) local nId := Val( oReq:Param( "id" ) ) + 1 if nId >= 1 .and. nId <= Len( aTodos ) ADel( aTodos, nId ) ASize( aTodos, Len( aTodos ) - 1 ) endif oRes:SendJSON( '{"status":"ok"}' ) return nil
Step 5: Refresh the Desktop Grid
static function RefreshGrid( oBrw ) local aData := {}, n for n := 1 to Len( aTodos ) AAdd( aData, { n, aTodos[ n ][ 1 ], aTodos[ n ][ 2 ] } ) next oBrw:SetArray( aData ) oBrw:Refresh() return nil
Step 6: Build and Test
- Press F9 to build and run.
- The desktop form appears with an empty task list. Add a few tasks using the GUI.
- Open a browser and navigate to
http://localhost:8080. - You see the same tasks rendered as HTML. Add or delete tasks from the browser.
- Switch back to the desktop — the data is shared in real time.
Architecture Overview
TForm + TBrowse"] --- C["Shared Data
aTodos Array"] B["TWebServer
Port 8080"] --- C end D["Browser
http://localhost:8080"] -->|"HTTP GET/POST/DELETE"| B A -->|"Direct array access"| C style A fill:#58a6ff,stroke:#388bfd,color:#0d1117 style B fill:#3fb950,stroke:#2ea043,color:#0d1117 style C fill:#d2a8ff,stroke:#bc8cff,color:#0d1117 style D fill:#f0883e,stroke:#d18616,color:#0d1117
For larger web apps, use oServer:Static( "/assets", "./www" ) to serve CSS, JavaScript,
and image files from a local folder instead of embedding HTML in strings.
Ready to add intelligence to your app? Continue to the AI Integration tutorial to connect to local LLMs and run inference directly from your application.