The last several years of web development have been going into a direction of needlessly over-engineering even simplest tasks.
Maybe it’s time to get back to the basics, to use simple solutions for simple tasks. Let’s touch on how I think we should be approaching initial web product development.
First, let’s set the requirements for a web solution:
In order to achieve these, I am proposing a simple server side application, that is rendering the HTML on the server, has statical asset serving built in and is fairly simple from structure perspective.
In a more detailed manner, following the three dimensions above:
Let’s take a look at an exmple. You can find the entire repository on Github here and below we’ll take a look at the entry point of the server-side application.
package main
import (
"embed"
"html/template"
"log"
"net/http"
"main/handlers"
"main/middlewares"
)
//go:embed templates
var embededTemplates embed.FS
//go:embed public
var embededPublic embed.FS
func main() {
// pre-parse templates, embedded in server binary
handlers.Tmpl = template.Must(template.ParseFS(embededTemplates, "templates/layouts/*.html", "templates/partials/*.html"))
// mux/router
mux := http.NewServeMux()
// public HTML route middleware stack
publicHTMLStack := []middlewares.Middleware{
middlewares.Logger,
}
// HTML routes
mux.HandleFunc("/", middlewares.CompileMiddleware(handlers.Home, publicHTMLStack))
// static routes, embedded in server binary
mux.Handle("/public/", handlers.ServeEmbedded(http.FileServer(http.FS(embededPublic))))
// HTTP server
log.Fatal(http.ListenAndServe(":8000", mux))
}
Allow me to describe a little what I have done.
I am using no external dependencies, everything is implemented using the Go standard library.
I have moved request handlers, logging middleware into their own files for having a bit simpler server file. Templates and static files are also naturally in their own separate folders and files.
The rest of the import is part of the standard Go release, its standard library.
embededTemplates
and embededPublic
are the two variables where I’m holding references to the embedded files, templates and static files respectively.
Why is this needed? I think it’s simpler to ship a single binary to the production environment of your choice and being able to run it from anywhere on a filesystem, as opposed to shipping the entire directory and making sure it works from the folder you shipped it to on your production filesystem.
Here I pre-parse the Go templates, so that they are ready to use in request handlers.
The router initialisation comes next.
After that, I initialise my single middleware for the moment and later call it automatically in each request handler so that it logs my requests.
I only have two routes set up for the moment, the root route and static file serving one.
Then I start the server on port 8000. That’s it.
While this is simple Go server which I basically built for myself, covering several easy to use features, it has some missing points, like how do I ship it and where? That is coming in a next not post.
Till then, have a look at the repo, let me know what you think. To run it you could either install air and then do air .air.toml
or you could just simply do go run server.go
. Of these two, air
will give you the benefit of watching for file changes and restarting the server for you.
Almost forgot: run go build
and then ./main
to try out how it runs as a single binary, maybe even move main
around your filesystem to see if it works (it should).
Needless to say, for all of these to work, you need to have Go installed. I personally use asdf for that and other CLI tooling.