Fullstack Web App, part 1: Golang + Echo + Templ backend skeleton project

Echo terminal

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.

Github setup

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:

templ generate

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:

Echo running

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?

run make

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!

Leave a Reply

Your email address will not be published. Required fields are marked *