Live Demo
The application replicates exactly the one used in the book Hypermedia Systems (a tutorial about HTMX, HyperView and REST-fulness) but it's designed according to some self-imposed constraints and design principles.
The tutorial presents a single entity: the Contact; but I'm writing the project as if more will come.
<input
type="email"
name="Email"
placeholder="Email"
value="{{ .Email }}"
hx-patch="/contact/email"
hx-target="next .error" />
<span class="error"></span>
The input[type=email]
becomes an HTML control that will fire a PATCH /contact/email
on the default event changed
(i.e. focus moved to the nest element and content changed).
The hx-target="next .error"
instructs the browser to replace the following span
element with the response; the next .error
is HyperScript code.
More examples and excellent docs about HTMX can be found on the HTMX docs homepage.
The tutorial uses the case of an address-book CRUD application to demonstrate how to:
- turn a traditional full-page refresh web-app into a smoother UI\UX (no full-page refresh) without changing the server-side implemntation, by just adding the
hx-boost
to the HTML body - implement a simple search functionality
- implement pagination with on-demand or continuous scrolling
- send request with HTTP methods not natively supported by HTML (eg. DELETE)
- implement server-side valiadation of an individual fields and display of validation response
- see how HTMX facilitates a REST-ful (as Fielding's dissertation) architecture
- practice fundamentals of web development in Go and its standard library
- learn fundamentals of HTMX
- draft a DDD-inspired project structure
- be a production-realistic example (eg. don't worry about authentication, security, observability, testing, etc.)
- precise validation and error reporting
- data-persistance
- …etc
-
use only Go stdlib
- therefore, I haven't used paths pattern-matching (eg.
/myentity/:id/property
). Instead I pass all the dynamic values in the URL query - no reflection or generic logic to associate http-handlers with http methods. Instead I use a
switch-case
on the http-method value.
- therefore, I haven't used paths pattern-matching (eg.
-
no hardcoded application URLs in the HTML templates…
all URL's are passed (pre-computed if dynamic) as template parameters. Also see the Design Principles
-
only implement the functionality presented in the tutorial or less (but I may implement it differently, eg. by using a different HTTP method)
-
only an in-memory db, but design program against an interface (not a concrete implementation)
-
the DB interface stands for a component performing I/O and so should accept a
context
and return an error in all methods; I haven't bothered because of the My Non-Goals -
no tests (unless for exploratory reasons)
-
just-enough CSS
Aesthetic is not a goal here but we also don't want our eyes to bleed; so I just styled it with PicoCSS with default settings… and semantic HTML is all I need write, sweet.
-
tidy-up templates setup later
I'm not happy with how I'm parsing and loading templates but I haven't found way that seems idiomatic, logical, and is optimal (parse each template only once and then compose the parsed-trees)
-
git commit messages are a single line and only meant as short-term reminders
-
application code is grouped by entity
all code that represents that entity lives within that entity folder (incl. http handlers, html templates, etc.)
-
template rendering API…
- template files are embedded
embed.FS
in the application and collected in dedicated package - each package of templates exposes one
Write_TemplateName_(io.Writer, TemplateParams)
functions per templates. The function accepts a struct consisting of all the parameters required or supported by the template. - a
templates
package sits at the root of the project and contains the common HTML layout code
- template files are embedded
-
let the handler require which kinds of URL it will support (eg. listing, viewing, editing) and what parameters it will expect; let the HTTP server setup code decide the specific URL to use
- I often use the name
me
ormy
for the method receiver… - I have sometimes used else-blocks for the happy path where it's often advised to keep the happy path at "indentation level zero".