Version: 6.99
A B4J library for building REST APIs with the Pakai Server framework. Provides HTTP request/response handling, JSON/XML parsing, cookie management, authentication, file uploads, HTML templating, and auto-generated OpenAPI 3.0 documentation with Swagger UI integration.
The WebApiUtils.b4xlib contains:
| File | Description |
|---|---|
WebApiUtils.bas |
Core utility module — request parsing, response helpers, auth, validation, templating, file I/O |
HelpHandler.b4x_excluded |
Auto-generated API documentation handler (available as a Custom Class Template in the IDE) |
Snippets/Handler (Api).txt |
Code snippet template for creating new REST API handlers |
Snippets/Handler (Web).txt |
Code snippet template for creating new Web UI handlers |
LICENSE |
MIT License |
Note: Other modules like
ORM.bas,MH.bas,MC.bas,ProductsHandler,CategoriesHandler, etc. are part of the Pakai Server template project, not this library.
| Library | Version | Purpose |
|---|---|---|
| JavaObject | 2.07 | Java interop |
| Json | 1.30 | JSON parsing |
| jStringUtils | 1.03 | String utilities |
| Xml2Map | 1.01 | XML parsing |
- Unified JSON/XML Response Envelope — Consistent format with configurable key names
- Ordered JSON/XML Serialization — Preserves key order via
__ordermetadata - Request Parsing — JSON, XML, URL-encoded, multipart form data and file uploads
- Authentication Helpers — Basic Auth, Bearer Token, API Key extraction
- Cookie Management — Read/write cookies with SameSite, HttpOnly, Secure flags
- CSRF Protection — Token validation utilities
- HTML Templating — Token replacement (
$KEY$,@VIEW@,@TAG@) - Validation — Email, content type (JSON/XML), integer, HTTP method validation
- Auto-Generated API Documentation — Interactive API console served by
HelpHandlerwith OpenAPI 3.0 JSON spec - Swagger UI Integration — Works with the Swagger UI distribution for a full OpenAPI explorer
- Code Snippets — Template files for rapidly scaffolding new API and Web handlers
Use the Pakai Server B4J template or create a new B4J server project.
Copy release/WebApiUtils.b4xlib to your B4J additional libraries folder, or use the IDE's library manager.
The library registers a Custom Class Template named Help Handler — you can add HelpHandler to any project via Project → Add New Module → Help Handler.
HelpHandler is a class that:
- Builds an interactive HTML documentation page at
/help(powered by Alpine.js + HTMX) - Serves an OpenAPI 3.0 JSON specification at
/help?format=openapi - Generates ready-to-use B4X client code snippets at
/help?format=snippetsfor every registered endpoint
' In your main server module
App.Get("/help", "HelpHandler")To register API handlers for documentation discovery, add your handler name to the Handlers list in HelpHandler.Initialize:
Public Sub Initialize
Handlers.Initialize
Handlers.Add("ProductsApiHandler")
Handlers.Add("CategoriesApiHandler")
Handlers.Add("FindApiHandler")
End SubThe BuildMethods sub (HelpHandler.bas:307) is the recommended way to define endpoint metadata for both the interactive /help page and the OpenAPI 3.0 JSON spec at /help?format=openapi. Unlike ReadHandlers which parses #hashtag comments from source code at compile time (Debug mode only), BuildMethods constructs endpoint properties programmatically and works in both Debug and Release modes without source file access.
Each endpoint is represented as a Map of properties. You retrieve an existing entry, modify its properties, and replace it. The key helper methods:
| Method | Purpose |
|---|---|
RetrieveMethod(Group, SubName) |
Gets the method's default Map (auto-populated by parsing the sub signature). If not found, calls CreateMethodProperties to build a new Map. |
CreateMethodProperties(Group, SubName) |
Creates a fresh Map with auto-detected Verb (from sub name prefix like Get, Post, Put, Delete or #hashtag overrides) and Parameters (from sub arguments). |
ReplaceMethod(Method) |
Finds existing entry by method name and replaces it in-place (keeping list order). |
RemoveMethodAndReAdd(Method) |
Removes then appends to end of list (for ordering control). |
FindMethod(MethodName) |
Returns the index of a method in AllMethods, or -1. |
| Key | Description |
|---|---|
Group |
Heading/group name for categorizing endpoints (e.g., "Products", "Categories") |
Name |
URL path segment override (defaults to Group). Used for the /api/{name}/... route |
Method |
Sub name (read-only identifier for matching) |
Desc |
Description text shown in the accordion header |
Verb |
HTTP method: GET, POST, PUT, DELETE |
Params |
Parameter display string like "id [Int]" or "Not required" |
Format |
JSON string showing the expected request body format (displayed as a sample) |
Body |
JSON string with default values for the request body editor |
Elements |
JSON array string for URL path elements (e.g., ["{id}"]) |
Noapi |
Boolean — set True for routes outside /api/ (uses $SERVER_URL$/ instead) |
Expected |
HTML string describing expected response codes |
FileUpload |
File upload type ("image", "pdf") — shows upload UI in the docs page |
Authenticate |
Auth type ("basic", "token", "apikey") — shows auth badge |
Private Sub BuildMethods
' --- Using RetrieveMethod (auto-detects from sub signature) ---
Dim Method As Map = RetrieveMethod("Products", "GetProducts")
Method.Put("Desc", "Read all Products")
ReplaceMethod(Method)
' --- Adding path element (URL variable) ---
Dim Method As Map = RetrieveMethod("Products", "GetProductById (id As Int)")
Method.Put("Desc", "Read one Product by id")
Method.Put("Elements", $"["{id}"]"$)
ReplaceMethod(Method)
' --- Creating from scratch with CreateMethodProperties ---
' Useful when there is no matching sub (e.g. sub name doesn't start with a verb)
' The Verb will be inferred from the sub name prefix (Get, Post, Put, Delete)
' or from #hashtag overrides like '#POST
Dim Method As Map = CreateMethodProperties("Products", "PostProduct")
Method.Put("Desc", "Add new Product")
Dim FormatMap As Map = CreateMap("category_id": 3, "product_code": "E001", "product_name": "Wireless Mouse", "product_price": 29.99)
Method.Put("Format", FormatMap.As(JSON).ToString)
Method.Put("Body", FormatMap.As(JSON).ToString)
ReplaceMethod(Method)
' --- POST/PUT endpoints with request body examples ---
Dim Method As Map = RetrieveMethod("Find", "SearchByKeywords ' #post")
Dim FormatMap As Map = CreateMap("keyword": "text")
Dim BodyMap As Map = CreateMap("keyword": "")
Method.Put("Format", FormatMap.As(JSON).ToString)
Method.Put("Body", BodyMap.As(JSON).ToString)
Method.Put("Desc", "Filter Products (with Category name)")
Method.Put("Expected", GetExpectedResponse(""))
ReplaceMethod(Method)
' --- Move an endpoint to the end of list ---
Dim Method As Map = RetrieveMethod("Categories", "DeleteCategoryById (id As Int)")
Method.Put("Desc", "Delete Category by id")
Method.Put("Elements", $"["{id}"]"$)
RemoveMethodAndReAdd(Method)
End SubThe ServeOpenApiJson sub (HelpHandler.bas:1200) iterates through the same AllMethods list populated by BuildMethods. For each entry it:
- Calls
GenerateVerbSectionto extract Verb, Link, Description, Params, Format, Body - Builds dynamic OpenAPI path parameters from
{id}/{category}placeholders in the URL - Constructs
requestBodyschemas for POST/PUT endpoints with field names and types - Assembles the full OpenAPI 3.0 JSON root with
openapi,info, andpathskeys
This means any endpoint defined in BuildMethods automatically appears in both the interactive /help page and the Swagger UI /swagger/ page.
The /help?format=snippets endpoint (HelpHandler.bas:48) generates ready-to-use B4X client subroutines. It iterates through AllMethods (built by BuildMethods) and calls GenerateAppSnippet for each one, returning plain text B4X code.
GenerateAppSnippet (HelpHandler.bas:1427) builds a complete HttpJob-based subroutine for every endpoint:
' --- SNIPPET FOR: [GET] /api/products ---
' Description: Read all Products
Public Sub GetProducts
Dim j As HttpJob
j.Initialize("", Me)
j.Download(BaseURL & "/api/products")
Wait For (j) JobDone(j As HttpJob)
If j.Success Then
Log(j.GetString)
End If
j.Release
End Sub
' --- SNIPPET FOR: [POST] /api/products ---
' Description: Add new Product
Public Sub PostProduct (JsonBody As String)
Dim j As HttpJob
j.Initialize("", Me)
j.PostString(BaseURL & "/api/products", JsonBody)
j.GetRequest.SetContentType("application/json")
Wait For (j) JobDone(j As HttpJob)
If j.Success Then
Log(j.GetString)
End If
j.Release
End SubThe snippet generator reads the same Method Map properties from BuildMethods:
| Map Key | Effect on generated snippet |
|---|---|
Group |
Categories endpoints into groups (comment header) |
Desc |
Becomes a ' Description: comment |
Verb |
Determines j.Download (GET) vs j.PostString (POST/PUT) |
Elements |
Path variables like {id} become subroutine parameters |
Authenticate |
When "token", adds AuthToken As String parameter and Authorization: Bearer header |
Name |
URL path segment used in the generated URL |
To use the generated code in a B4J/B4A client app:
- Visit
/help?format=snippetsin a browser - Copy the generated subroutines
- Paste into your client project (replace
BaseURLwith your server URL)
The HelpHandler serves OpenAPI 3.0 JSON at /help?format=openapi and B4X client code snippets at /help?format=snippets. The Swagger UI is configured as a split-pane layout with the interactive API explorer on the left and a live B4X code snippet panel on the right.
To set it up:
- Download the Swagger UI dist folder
- Place it in your project at
Objects/www/swagger/ - Use the modified
swagger-initializer.js:
window.onload = function () {
const ui = SwaggerUIBundle({
url: "../help?format=openapi",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
window.ui = ui;
// Fetch B4X code snippets from HelpHandler
setTimeout(injectDesktopToggleButton, 1000);
refreshB4XCode();
};- Use the modified
index.htmlwhich provides:- Left pane — Standard Swagger UI interactive console
- Right sidebar — Live B4X code snippets fetched from
../help?format=snippets - Sync button — Refreshes snippets from the server
- Copy All button — Copies all snippets to clipboard
- Toggle button — Shows/hides the snippet sidebar (desktop: inline toggle, mobile: slide-out drawer)
- Dark mode toggle — Moon/sun button for mobile
<!-- The split-pane layout structure -->
<div style="display: flex; height: 100vh; width: 100vw;">
<!-- LEFT: Swagger Console -->
<div style="flex: 1; overflow-y: auto;">
<div id="swagger-ui"></div>
</div>
<!-- RIGHT: B4X Snippet Sidebar -->
<div id="b4x-sidebar" style="width: 480px; background: #282c34;">
<pre><code id="b4x-code-box">Syncing network templates...</code></pre>
</div>
</div>- Navigate to
/swagger/to access the Swagger UI explorer with the B4X snippet sidebar
' Central response handler — formats JSON or XML with envelope keys
WebApiUtils.ReturnHttpResponse(HRM As HttpResponseMessage, Response As ServletResponse)
' Shorthand HTTP responses
WebApiUtils.ReturnSuccess(Message, Response, Code, Data)
WebApiUtils.ReturnError(Message, Response, Code, Error)
WebApiUtils.ReturnConnect(Message, Response)
WebApiUtils.ReturnBadRequest(Message, Response) ' 400
WebApiUtils.ReturnAuthorizationRequired(Message, Response) ' 401
WebApiUtils.ReturnTokenExpired(Message, Response) ' 401
WebApiUtils.ReturnMethodNotAllow(Message, Response) ' 405
WebApiUtils.ReturnErrorUnprocessableEntity(Message, Response) ' 422
WebApiUtils.ReturnErrorCredentialNotProvided(Message, Response) ' 400
WebApiUtils.ReturnErrorExecuteQuery(Message, Response) ' 422
WebApiUtils.ReturnLocation(Location, Response) ' 302 redirect
WebApiUtils.ReturnContent(Content, ContentType, Response) ' Raw content
WebApiUtils.ReturnHtml(Str, Response) ' HTML page
WebApiUtils.ReturnHtmlBody(Cont, Response) ' HTML from HttpResponseContent
WebApiUtils.ReturnHtmlPageNotFound(Response) ' HTML 404 page
WebApiUtils.ReturnHtmlBadRequest(Response) ' HTML 400 page
WebApiUtils.ReturnHtmlMethodNotAllowed(Response) ' HTML 405 page
WebApiUtils.ReturnOutputStream(Ins, Response) ' Binary stream
WebApiUtils.ReturnFileInline(Ins, FileName, Response) ' Inline file
WebApiUtils.ReturnFileAttachment(Ins, FileName, Response) ' Download attachment| Field | Type | Description |
|---|---|---|
ResponseCode |
Int | HTTP status code |
ResponseMessage |
String | Success/status message |
ResponseStatus |
String | "success" or "error" |
ResponseType |
String | Response type identifier |
ResponseError |
Object | Error details |
ResponseData |
List | Collection result |
ResponseObject |
Map | Single object result |
ResponseBody |
Object | Raw response body |
PayloadType |
String | "application/json" or "application/xml" |
ContentType |
String | Response content type |
XmlRoot |
String | XML root element name |
XmlElement |
String | XML element name |
VerboseMode |
Boolean | Include debug fields |
OrderedKeys |
Boolean | Preserve key ordering |
ResponseKeys |
List | Custom envelope key names |
ResponseKeysAlias |
List | Envelope key aliases |
' Read request body
Dim text As String = WebApiUtils.RequestDataText(Request) ' Raw text
Dim json As Map = WebApiUtils.RequestDataJson(Request) ' JSON → Map
Dim xml As Map = WebApiUtils.RequestDataXml(Request) ' XML → Map
Dim data As Map = WebApiUtils.RequestData(Request) ' JSON (alias)
' Parse strings
Dim parsedMap As Map = WebApiUtils.ParseJSON(text)
Dim parsedMap As Map = WebApiUtils.ParseXML(text)
' Multipart uploads
Dim part As Part = WebApiUtils.RequestMultiPart(Request, Folder, MaxSize)
Dim parts As List = WebApiUtils.RequestMultipartList(Request, Folder, MaxSize)
Dim formData As Map = WebApiUtils.RequestMultipartData(Request, Folder, MaxSize)
' Cookies
Dim cookies As Map = WebApiUtils.RequestCookie(Request)' Extract credentials
Dim auth As Map = WebApiUtils.RequestBasicAuth(AuthsList) ' Basic Auth
Dim token As String = WebApiUtils.RequestBearerToken(Request) ' Bearer token
Dim apiKey As String = WebApiUtils.RequestApiKey(Request) ' X-API-Key header
Dim accessToken As String = WebApiUtils.RequestAccessToken(Request) ' Authorization header' Simple cookie
WebApiUtils.ReturnCookie(Key, Value, MaxAge, HttpOnly, Response)
' Full control
WebApiUtils.ReturnCookie2(Key, Value, Path, SameSite, MaxAge, HttpOnly, Secure, Response)Dim encoded As String = WebApiUtils.EncodeURL(str)
Dim decoded As String = WebApiUtils.DecodeURL(str)
Dim b64 As String = WebApiUtils.EncodeBase64(bytes)
Dim bytes() As Byte = WebApiUtils.DecodeBase64(str)Dim valid As Boolean = WebApiUtils.Validate_Email("user@example.com")
Dim valid As Boolean = WebApiUtils.ValidateContent(text, WebApiUtils.MIME_TYPE_JSON)
Dim valid As Boolean = WebApiUtils.CheckInteger(value)
Dim valid As Boolean = WebApiUtils.CheckAllowedVerb(supportedMethods, method)
Dim valid As Boolean = WebApiUtils.CheckMaxElements(elements, maxCount)
Dim valid As Boolean = WebApiUtils.ValidateCsrfToken(sessionName, headerName, Request)' Replace $KEY$ tokens with Map values
Dim result As String = WebApiUtils.BuildHtml(template, settings)
' Replace @VIEW@, @DOCVIEW@, @SCRIPT@, @KEY@ placeholders
Dim result As String = WebApiUtils.BuildView(html, viewContent)
Dim result As String = WebApiUtils.BuildDocView(html, docViewContent)
Dim result As String = WebApiUtils.BuildScript(html, scriptContent)
Dim result As String = WebApiUtils.BuildScript2(html, script, settings)
Dim result As String = WebApiUtils.BuildTag(html, "KEY", value)
Dim result As String = WebApiUtils.BuildCsrfToken(html, token)
' Regex-based $KEY$ replacement
Dim result As String = WebApiUtils.ReplaceMap(base, replacements)Dim text As String = WebApiUtils.ReadTextFile("file.txt") ' From assets
WebApiUtils.WriteTextFile("file.txt", contents) ' To File.DirApp
WebApiUtils.WriteAssetFile("file.txt", contents) ' To www/
WebApiUtils.DeleteAssetFile("file.txt")
Dim exists As Boolean = WebApiUtils.AssetFileExist("file.txt")
Dim map As Map = WebApiUtils.ReadMapFile(File.DirApp, "config.ini")Dim guid As String = WebApiUtils.GUID ' New GUID
Dim dt As String = WebApiUtils.CurrentDateTime ' yyyy-MM-dd HH:mm:ss
Dim ts As String = WebApiUtils.FileNameByCurrentDateTime ' yyyyMMddHHmmss
Dim tz As String = WebApiUtils.GetCurrentTimezone ' Timezone offset
Dim json As String = WebApiUtils.Object2Json(obj) ' Object → JSON string
Dim proper As String = WebApiUtils.ProperCase("word") ' Capitalize first letter
Dim escaped As String = WebApiUtils.EscapeHtml("<tag>") ' HTML entity escape
Dim escaped As String = WebApiUtils.EscapeXml("<tag>") ' XML entity escape
Dim clean As String = WebApiUtils.LinearizeXML(xmlText) ' Strip XML comments/whitespace
Dim cropped() As String = WebApiUtils.CropElements(full, startIndex) ' URL element cropping
Dim pathParts() As String = WebApiUtils.GetApiPathElements(path)
Dim uriParts() As String = WebApiUtils.GetUriElements(uri)WebApiUtils.MIME_TYPE_HTML = "text/html"
WebApiUtils.MIME_TYPE_JSON = "application/json"
WebApiUtils.MIME_TYPE_XML = "application/xml"
WebApiUtils.MIME_TYPE_PDF = "application/pdf"
WebApiUtils.MIME_TYPE_PNG = "image/png"WebApiUtils.RESPONSE_ELEMENT_MESSAGE = "m"
WebApiUtils.RESPONSE_ELEMENT_CODE = "a"
WebApiUtils.RESPONSE_ELEMENT_STATUS = "s"
WebApiUtils.RESPONSE_ELEMENT_TYPE = "t"
WebApiUtils.RESPONSE_ELEMENT_ERROR = "e"
WebApiUtils.RESPONSE_ELEMENT_RESULT = "r"HelpHandler originally supported #hashtag comments in handler source files to provide endpoint metadata. These are parsed by ReadHandlers → ParseHashtags at runtime.
⚠ Deprecated:
#hashtagparsing is Debug mode only (#If DebuginShowHelpPage), requires the.bassource files to be present at runtime, and is fragile (can break from simple comment changes). UseBuildMethodsinstead — it works in both Debug and Release, doesn't depend on source files, and gives full control over all endpoint properties.
ReadHandlers (HelpHandler.bas:390) scans each registered handler's .bas file line-by-line:
- Lines containing
Subwith a verb prefix (Get,Post,Put,Delete) or#VERBoverride are picked up as endpoint entries - Subsequent comment lines containing
#keyword=valueare passed toParseHashtagswhich attaches metadata to the last detected method - Boolean-only hashtags (
#hide,#noapi) are set toTruewhen present
| Keyword | Type | Example |
|---|---|---|
#name |
=value |
#name = Products (URL path segment) |
#desc |
=value |
#desc = List all products (accordion header text) |
#version |
=value |
#version = 2.0 (version suffix) |
#elements |
=value |
#elements = ["{id}"] (path elements as JSON array) |
#body |
=value |
#body = {"key": ""} (JSON body for editor) |
#format |
=value |
#format = {"key": "value"} (JSON format example) |
#fileupload |
=value |
#fileupload = image (shows upload UI) |
#authenticate |
=value |
#authenticate = token (shows auth badge) |
#hide |
flag | hides the endpoint from the docs page |
#noapi |
flag | endpoint is outside /api/ path |
'#POST /api/products
' Creates a new product in the database
Private Sub PostProduct
'#desc = Add new Product
'#authenticate = token
'#elements = []
'#body = {"category_id": 1, "product_code": "E001", "product_name": "Wireless Mouse", "product_price": 29.99}
'#format = {"category_id": 3, "product_code": "CODE", "product_name": "Name", "product_price": 0}For Release builds, convert to BuildMethods:
Dim Method As Map = CreateMethodProperties("Products", "PostProduct")
Method.Put("Desc", "Add new Product")
Method.Put("Authenticate", "token")
Dim FormatMap As Map = CreateMap("category_id": 3, "product_code": "E001", "product_name": "Wireless Mouse", "product_price": 29.99)
Dim BodyMap As Map = CreateMap("category_id": 3, "product_code": "", "product_name": "", "product_price": 0)
Method.Put("Format", FormatMap.As(JSON).ToString)
Method.Put("Body", BodyMap.As(JSON).ToString)
ReplaceMethod(Method)Using the Handler (Api).txt snippet template in the IDE.
' MyModel.bas
Sub Class_Globals
Private DB As MiniORM
End Sub
Public Sub Initialize
DB = Main.DB
End Sub
Public Sub Read As List
DB.Open
DB.Table = "tbl_my_entities"
DB.Query
Return DB.Results
End Sub
Public Sub Create (Name As String)
DB.Open
DB.Table = "tbl_my_entities"
DB.Columns = Array("name")
DB.Parameters = Array(Name)
DB.ReturnRow = True
DB.Save
End Sub' MyApiHandler.bas
Sub Class_Globals
Private Path As String
Private Method As String
Private Request As ServletRequest
Private Response As ServletResponse
Private HRM As HttpResponseMessage
Private Model As MyModel
End Sub
Public Sub Initialize
HRM = Main.HRM
Model.Initialize
End Sub
Sub Handle (req As ServletRequest, resp As ServletResponse)
Request = req
Response = resp
Path = Request.RequestURI
Method = Request.Method.ToUpperCase
If Path = "/api/myentities" And Method = "GET" Then
GetEntities
Else If Path = "/api/myentities" And Method = "POST" Then
PostEntity
Else If Path.StartsWith("/api/myentities/") And Method = "GET" Then
GetEntityById
Else If Path.StartsWith("/api/myentities/") And Method = "PUT" Then
PutEntityById
Else If Path.StartsWith("/api/myentities/") And Method = "DELETE" Then
DeleteEntityById
Else
WebApiUtils.ReturnBadRequest(HRM, Response)
End If
End Sub
Private Sub GetEntities
Dim Data As List = Model.Read
If Model.Error.IsInitialized Then
HRM.ResponseCode = 422
HRM.ResponseError = Model.Error.Message
Else
HRM.ResponseCode = 200
HRM.ResponseData = Data
End If
WebApiUtils.ReturnHttpResponse(HRM, Response)
End SubApp.Rest("/api/myentities/*", MyApiHandler)
' Or selectively:
' App.Route("/api/myentities", MyApiHandler, Array("get", "post"))
' App.Get("/api/myentities/*", MyApiHandler)
' etc.Add your handler name to the Handlers list in HelpHandler.Initialize, then define endpoint metadata in BuildMethods:
' In HelpHandler.Initialize
Handlers.Add("MyApiHandler")
' In HelpHandler.BuildMethods
Dim Method As Map = RetrieveMethod("MyEntity", "GetEntities")
Method.Put("Desc", "List all entities")
ReplaceMethod(Method)All API responses use a consistent envelope. Default key names:
{
"a": 200,
"s": "success",
"m": "Products retrieved successfully",
"e": null,
"r": [ { "id": 1, "product_name": "Hammer" } ]
}{
"a": 422,
"s": "error",
"m": "Query execution error",
"e": "Column 'xyz' not found",
"r": null
}Keys can be customized via ResponseKeys and ResponseKeysAlias in HttpResponseMessage.
The library includes two snippet templates accessible from the B4J IDE:
A full CRUD API handler template with route dispatching for GET, POST, PUT, DELETE. Uses $Endpoint$ / $endpoints$ placeholders for find-and-replace. Includes:
- Input validation (required keys, content type)
- Conflict detection (unique code checks)
- Standardized response formatting
A Web UI handler template using HTMX partial page updates. Includes:
- Route dispatching for page, table, add/edit/delete modals
- Table rendering with search/filter support
- Form validation
- Toast notifications via custom events
MIT License — Copyright (c) 2022-2026 Poon Yip Hoon (Aeric). See LICENSE.