Over the past months I’ve developed an affirmations web app using this cool stack: Golang + Echo + Templ.
If you’re here you already know about Golang…but what about Echo and Templ?
Well, echo is a “High performance, extensible, minimalist Go web framework”, definition taken from their website. It has a lot of cool features that allow you to quickly build a web app or a REST API.
Templ on the other hand allows you to “Create components that render fragments of HTML and compose them to create screens, pages, documents, or apps.” It’s a template rendering engine, allowing you to dynamically render html pages.
In this tutorial I’m going to walk you through building a similar, albeit simpler web app using this stack.
You can download the source code from this tutorial from my github repo.
Let’s begin!
Project setup
For this tutorial I’m going to be using Linux but you can follow along if you’re on Windows, using similar commands for making directories and new files.
Let’s start by creating a directory for our project and navigating inside it:
mkdir templ_echo_app
cd templ_echo_app/
I like to create my project structure beforehand, so everything is nice and tidy from the beginning. I took inspiration from this guide on how to organize a go project and adapted it to my own needs and preferences.
First of all let’s make a cmd directory and an entry point .go file to our app
mkdir cmd
touch cmd/main.go
Next let’s create an internal directory to hold private library code, that cannot be imported by external apps. In here we’ll make a model directory for our db operations and a view directory for our .templ files (we’ll use templ later to render them to html). Inside the view directory we’ll have a page directory for holding .templ pages, later we’ll add other directories for components or sections.
mkdir -p internal/view/page
mkdir internal/model
Github setup
It’s a good idea to add version control to our project asap. The most popular choice is git+github, which I’ll use here. You probably already have a github account, if not, create one – it’s easy and free.
As for git, you can follow the install instructions to add it to your system.
Create a new github repo. I’ll make it public so you can access the source code, but you might want to make it private.
Init git. It’s also a good idea to create a .gitignore file.
git init
touch .gitignore
.gitignore should have the following content below. Use your favorite editor for this, I use neovim but VsCode is another popular option.
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
tmp/
# Go workspace file
go.work
Next use this to track our content and create a first commit:
git add .
git commit -m "my first commit"
In our root directory of our app, connect git to our newly created repo. My repo link is git@github.com:adrianlarion/templ_echo_app.git
but for you it will be different.
git remote add origin <YOUR_REPO_LINK>
It’s worth mentioning that in order for this to work you’ll have to setup github to work with ssh. You can follow the instructions here. But you can skip this step and only have git keep track of your source code locally (not recommended but possible).
Push to github:
git push origin main
Afterwards it’s a good idea to making commits as you change code, maybe create new branches for dev and various features, etc. I’ll leave this to your best judgement.
Set up Go
Create a module inside your root directory, using your github repo link as name. My link is github.com/adrianlarion/templ_echo_app
go mod init <YOUR_REPO_LINK>
Install echo:
$ go get github.com/labstack/echo/v4
Install echo middleware, we’ll use this for adding some useful features to our app:
github.com/labstack/echo/v4/middleware
Install templ:
go install github.com/a-h/templ/cmd/templ@latest
Create new files
Now let’s create a bunch of new .go files for our needs. We’ll need a handler file, a helper file and a couple of templ files. Handler file will handle routes, helper file will have utility functions, base templ will have the base html for our web app and home templ will use base templ to create a home page.
If it sounds confusing, don’t worry, you’ll see things in action and understand better.
touch cmd/handler.go
touch cmd/helper.go
mkdir internal/view/layout
touch internal/view/layout/base.templ
touch internal/view/page/home.templ
Edit template files
Open up base.templ and add the following content below. Basically this is standard html into which we’ll inject other html content.
package layout
templ Base(){
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta http-equiv="X-UA-Compatible" content="ie=edg"/>
</head>
<body>
{ children... }
</body>
</html>
}
Now open home.templ and add the content below. We’ll just render a simple paragraph to check that things are working
package page
import(
"github.com/adrianlarion/templ_echo_app/internal/view/layout"
)
templ Home(name string){
@layout.Base(){
<p>Hello from templ { name }</p>
}
}
Now run this command to generate go code from templ files in your root directory:
templ generate
You should see something like:
Render html with echo
Now let’s add a helper func to help us in rendering these templ files with echo:
Edit helper.go as indicated below:
package main
import (
"github.com/a-h/templ"
"github.com/labstack/echo/v4"
)
func render(ctx echo.Context, statusCode int, t templ.Component) error {
ctx.Response().Writer.WriteHeader(statusCode)
ctx.Response().Header().Set(echo.HeaderContentType, echo.MIMETextHTML)
return t.Render(ctx.Request().Context(), ctx.Response().Writer)
}
Now let’s edit our main.go file. It seems that a lot it’s going on but if we break it up it’s simple.
package main
import (
"context"
"net/http"
"os"
"os/signal"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/gommon/log"
)
type application struct{
}
func main(){
esrv := echo.New()
esrv.Logger.SetLevel(log.DEBUG)
app := &application{}
//--------------------------
//middleware provided by echo
//--------------------------
//panic recover
esrv.Use(middleware.Recover())
//body limit
esrv.Use(middleware.BodyLimit("35K"))
//secure header
esrv.Use(middleware.Secure())
//timeout
esrv.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Timeout: 5 * time.Second,
}))
//logger
esrv.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: `{"time":"${time_rfc3339_nano}","remote_ip":"${remote_ip}",` +
`"host":"${host}","method":"${method}","uri":"${uri}",` +
`"status":${status},"error":"${error}","latency_human":"${latency_human}"` +
`` + "\n",
CustomTimeFormat: "2006-01-02 15:04:05.00000",
}))
//routes
esrv.GET("/", app.home)
//graceful shutdown
ctx, stop := signal.NotifyContext(context.Background(),os.Interrupt)
defer stop()
go func(){
if err:= esrv.Start(":"+*port); err != nil && err != http.ErrServerClosed{
esrv.Logger.Fatal("shutting down server")
}
}()
<-ctx.Done()
ctx, cancel := context.WithTimeout(context.Background(),10*time.Second)
defer cancel()
if err := esrv.Shutdown(ctx); err != nil{
esrv.Logger.Fatal(err)
}
}
We’ll use the application
struct for dependency injection (to basically share common resources among various functions).
Next you can see that we’re using middleware. These functions allow you to intercept and modify HTTP requests and responses. We have some standard stuff for security, limiting the request body size, recovery in case of errors and a logger so that we can see what’s going on.
Afterwards we’ll set up our routing, /
will call the home
func (which we’ll create in just a bit). That is, when we’ll visit mywebsite.com/, we’ll call the home
func to render html.
Finally there is some code to manage graceful shutdown of our app.
Let’s create our home
func in our handler.go file:
package main
import (
"net/http"
"github.com/adrianlarion/templ_echo_app/internal/view/page"
"github.com/labstack/echo"
)
func (app *application) home(c echo.Context) error{
return render(c, http.StatusOK, page.Home("Adrian Larion"))
}
Above, we call our .templ page component and our helper render func to render a simple paragraph with our name.
Run our app:
Phew, that was a lot of boilerplate. Now let’s see if it will run, fingers crossed. But first let’s create a dir to hold our executable:
mkdir tmp
Now run the build command:
go build -o ./tmp/main ./cmd/...
Aaand we have an error. Ooops!
github.com/adrianlarion/templ_echo_app/cmd
cmd/handler.go:11:16: cannot use c (variable of type "github.com/labstack/echo".Context) as "github.com/labstack/echo/v4".Context value in argument to render: "github.com/labstack/echo".Context does not implement "github.com/labstack/echo/v4".Context (wrong type for method Echo)
have Echo() *"github.com/labstack/echo".Echo
want Echo() *"github.com/labstack/echo/v4".Echo
It seems we forgot to use v4
at the end of our echo import, in handler.go file. Let’s fix it, here’s the new handler file:
package main
import (
"net/http"
"github.com/adrianlarion/templ_echo_app/internal/view/page"
"github.com/labstack/echo/v4"
)
func (app *application) home(c echo.Context) error{
return render(c, http.StatusOK, page.Home("Adrian Larion"))
}
Let’s run the build command again
go build -o ./tmp/main ./cmd/...
And we have a fresh error
cmd/main.go:56:28: undefined: port
port
should be the port used by echo, but right now it doesn’t exist. We could create it manually but good practice is to set it as a flag. Modify the main.go file to add flag parsing (flags are passed in the command line when calling our executable, like myapp -port="4000"
)
package main
import (
"context"
"flag"
"net/http"
"os"
"os/signal"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/gommon/log"
)
type application struct{
}
func main(){
esrv := echo.New()
esrv.Logger.SetLevel(log.DEBUG)
//flag parsing
port := flag.String("port","4000","port for app")
flag.Parse()
app := &application{}
//--------------------------
//middleware provided by echo
//--------------------------
//panic recover
esrv.Use(middleware.Recover())
//body limit
esrv.Use(middleware.BodyLimit("35K"))
//secure header
esrv.Use(middleware.Secure())
//timeout
esrv.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Timeout: 5 * time.Second,
}))
//logger
esrv.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: `{"time":"${time_rfc3339_nano}","remote_ip":"${remote_ip}",` +
`"host":"${host}","method":"${method}","uri":"${uri}",` +
`"status":${status},"error":"${error}","latency_human":"${latency_human}"` +
`` + "\n",
CustomTimeFormat: "2006-01-02 15:04:05.00000",
}))
//routes
esrv.GET("/", app.home)
//graceful shutdown
ctx, stop := signal.NotifyContext(context.Background(),os.Interrupt)
defer stop()
go func(){
if err:= esrv.Start(":"+*port); err != nil && err != http.ErrServerClosed{
esrv.Logger.Fatal("shutting down server")
}
}()
<-ctx.Done()
ctx, cancel := context.WithTimeout(context.Background(),10*time.Second)
defer cancel()
if err := esrv.Shutdown(ctx); err != nil{
esrv.Logger.Fatal(err)
}
}
Run the build command again:
go build -o ./tmp/main ./cmd/...
And there are no more errors thrown. Yay! Here’s a cute dog picture as reward:
Now for the final test, let’s run our app:
./tmp/main
And we should see the familiar echo prompt appearing:
Now let’s visit our page. Open your browser and go to the link: http://localhost:4000/
You should see the following:
Hurray!
And if you’ll check the terminal you should see some nice logging showing our visit to the home page:
{"time":"2024-03-14T09:58:50.921708269+02:00","remote_ip":"::1","host":"localhost:4000","method":"GET","uri":"/","status":200,"error":"","latency_human":"38.674µs"
Now it may seem like a lot of boilerplate for such a simple result, but it’s necessary if we want to further build upon it and not have an unholy mess.
Making development easier:
Before we finish this tutorial, let’s make our development life easier by using some automation:
Start by terminating our server (Ctrl + C) in the terminal.
Next let’s add a Makefile so we can quickly build and run our app. The make
utility will use this to run our (future) elaborate build commands with a much simpler command. You can read more about it here or google for examples.
touch Makefile
Inside the Makefile add the following.
.PHONY: all run build_dev
all: build_dev run
build_dev:
@templ generate
go build -o ./tmp/main ./cmd/...
run:
@./tmp/main
Now in your root directory simply run the command:
make
And templ files will be generated, executable will be built and executable will be started, all with just one command. Pretty neat, huh?
Making development even easier:
Wouldn’t it be nice to have a hot reload feature? That is, when we make a change to our code and save, we’d like the build to happen automatically and for the executable to run.
There’s a nifty utility called air which does just that.
Install it
go install github.com/cosmtrek/air@latest
But before we edit the config for air, there’s another feature which I’d like. I’d like for the browser to refresh automatically after I save the code and the executable is built and run.
I’ll use a bash script to create this functionality. Create a new dir and inside it a bash script:
mkdir script
touch script/devreload.sh
The bash script should have the following (assuming I’m using google chrome, but if you’re using firefox change the name accordingly):
#!/bin/bash
CURRENT_WID=$(xdotool getwindowfocus)
WID=$(xdotool search --name "Google Chrome")
xdotool windowactivate $WID
sleep 0.8
xdotool key F5
xdotool windowactivate $CURRENT_WID
Now let’s create a config file for the air utility:
touch .air.toml
Inside the config file (.air.toml
)add the following:
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "make build_dev && ./script/devreload.sh"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go",".*_templ.go","app.js", ".*tmp"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl","templ", "html","scss","js"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true
Now just type in the terminal in your root directory:
air
And you should see make running and our executable starting.
Open again the browser at http://localhost:4000/
Now open the handler.go file and change the name:
package main
import (
"net/http"
"github.com/adrianlarion/templ_echo_app/internal/view/page"
"github.com/labstack/echo/v4"
)
func (app *application) home(c echo.Context) error{
return render(c, http.StatusOK, page.Home("Frodo Baggins"))
}
Save. You’ll see the make running again, focus changing to your browser and reloading, then back to your code editor.
That’s quite comfy, isn’t it? Now you can make the next Facebook. 🙂
You can get all the source code from this tutorial from my github repo.
Conclusion
This is it for this tutorial, I hope you enjoyed it. Leave a comment below to tell me what you think.
Want to work with me? Get in touch and I’ll get back to you asap!