Hi there. In this post, I’m going to go over a couple snippets of minimal HTTP servers that I use from time to time when I’m tinkering with Go.

Using net/http

For starters, here’s the boilerplate code I start with when I want to make a quick server with the net/http package:

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

func ping() http.Handler {
	type response struct {
		Message string `json:"message"`
	}
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		resp := response{Message: "pong"}
		w.WriteHeader(http.StatusOK)
		json.NewEncoder(w).Encode(resp)
	})
}

func main() {
	http.Handle("/ping", hello())
	http.ListenAndServe(":8080", nil)
}

This is great if you just need something with a pulse. But what about one that does a little bit more, like routing, middleware, and static files? This includes some of the patterns from my posts on the adapter pattern Part 1, Part 2, and Part 3. Here’s what that looks like flattened into one file:

package main

import (
	"embed"
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/gorilla/mux"
)

//go:embed static
var static embed.FS

type service struct {
	server      *http.Server
	router      *mux.Router
}

type serviceAdapter func(*service) *service

func adaptService(s *service, opts ...serviceAdapter) *service {
	for i := range opts {
		opt := opts[len(opts)-1-i]
		s = opt(s)
	}
	return s
}

func withServer(baseServer *http.Server) serviceAdapter {
	return func(s *service) *service {
		s.server = baseServer
		return s
	}
}

func withRouter(r *mux.Router) serviceAdapter {
	return func(s *service) *service {
		s.router = r
		return s
	}
}

func newService(opts ...serviceAdapter) *service {
	s := &service{}
	s = adaptService(s, opts...)
	return s
}

type handlerAdapter func(http.HandlerFunc) http.HandlerFunc

func adaptHandler(h http.HandlerFunc, opts ...handlerAdapter) http.HandlerFunc {
	for i := range opts {
		opt := opts[len(opts)-1-i]
		h = opt(h)
	}
	return h
}

func setContentType(content string) handlerAdapter {
	return func(next http.HandlerFunc) http.HandlerFunc {
		return func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", content)
			next(w, r)
		}
	}
}

func (s *service) setupHTTPRoutes() {
	// http basic routes
	s.router.Handle("/ping", adaptHandler(
		ping(),
		setContentType("application/json"),
	)).Methods("GET")
	// static files
	staticFS, err := fs.Sub(static, "static")
	if err != nil {
		log.Fatal(err)
	}
	fs := http.FileServer(http.FS(staticFS)))
	s.router.PathPrefix("/docs").Handler(http.StripPrefix("/static", fs))
}

func ping() http.Handler {
	type response struct {
		Message string `json:"message,omitempty"`
	}
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		resp := response{Message: "pong"}
		w.WriteHeader(http.StatusOK)
		json.NewEncoder(w).Encode(resp)
	})
}

func (s *service) ListenAndServe() error {
	return s.server.ListenAndServe()
}

func (s *service) Shutdown(ctx context.Context) error {
	return s.server.Shutdown(ctx)
}

func main() {
	srv := &http.Server{
		Handler: mux.NewRouter(),
		Addr:    ":8080",
	}
    service = NewService(
		withServer(srv),
		withRouter(router),
    )
    service.setupHTTPRoutes()

    // listen forever
	go func() {
		log.Printf("Starting the server on %s", service.server.Addr)
		if err := service.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("Error shutting down the server: %s", err)
		}
	}()

	// block for interrupt
	shutdown := make(chan os.Signal, 1)
	signal.Notify(shutdown, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
	<-shutdown
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if err := service.server.Shutdown(ctx); err != nil {
		log.Fatalf("Error shutting down the server: %s", err)
	}
}

This is a pretty extensible approach and I’ve been more or less happy building projects using this sort of scaffolding. The upside is that it’s all quite straightforward and you can add custom functionality/handlers very easily. This is one of the things I love about Go; you can write and extend applications like this really easily. However, reimplementing basic HTTP functionality (e.g., security middleware) for each project is error prone, and I have no motivation to maintain my own library of HTTP utilities when there’s already perfectly fine solutions out there.

Using echo

One of the nicer HTTP “frameworks” I’ve seen is Echo. I saw it “in the wild” when I was looking at the Temporal UI backend. The main thing I like about it is that you can quickly browse through the docs (and code!) to get a decent census of what you can do out of the box and get rolling with minimal friction. They’ve also got some great “cookbook” examples that show marginally more advanced patterns. This is in contrast to something like go-kit that requires a bit more cognitive load than is necessary for small hobby projects. Additionally, while the Gorilla toolkit will probably remain a mainstay in the ecosystem for the foreseeable future, it is in “archive” mode. If you’re on the fence about which toolkit to “git gud” with, Echo is worth consideration (note that even Echo relies on Gorilla under the hood in some places). Here’s a dead simple HTTP server example:

package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
)

func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":8080"))
}

Here’s a slightly more involved example that shows conventional uses of some middleware and is still remarkably straightforward:

package server

import (
	"fmt"
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

type Server struct {
	httpServer *echo.Echo
}

func NewServer() *Server {

	e := echo.New()

	// Middleware
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
		AllowOrigins: []string{"http://localhost:3000"},
		AllowHeaders: []string{
			echo.HeaderOrigin,
			echo.HeaderContentType,
			echo.HeaderAccept,
			echo.HeaderXCSRFToken,
			echo.HeaderAuthorization,
		},
		AllowCredentials: true,
	}))
	e.Use(middleware.Secure())
	e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
		CookiePath:     "/",
		CookieHTTPOnly: false,
		CookieSameSite: http.SameSiteStrictMode,
	}))
	s := &Server{
		httpServer: e,
	}
	return s
}

func (s *Server) Start() error {
	fmt.Println("Starting server on :8080...")
	s.httpServer.Logger.Fatal(s.httpServer.Start(":8080"))
	return nil
}

func (s *Server) Stop() {
	fmt.Println("Stopping server...")
	if err := s.httpServer.Close(); err != nil {
		s.httpServer.Logger.Warn(err)
	}
}

Rather than repeating examples from the documentation here, I’m just going to suggest you go check out the Echo docs. They’re quick to navigate and read, and they include a bunch of recipes and examples that show how easy it is to develop with Echo.

Bonus: Using Python + FastAPI

While I’m at it, I might as well show one last spiritually related example: my FastAPI starter snippet. This is mostly taken from the FastAPI docs.

First, set up your virtual env and dependencies:

pyenv virtualenv 3.9.1 python-fastapi-server
pyenv activate python-fastapi-server
pip install fastapi
pip install "uvicorn[standard]"

Then in main.py:

"""main.py"""
from typing import Union

from fastapi import FastAPI


app = FastAPI()


@app.get("/")
def index():
    """Root path."""
    return {"status": "ok"}


@app.get("/ping")
def ping():
    """Say hello."""
    return "pong"


@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    """Read item with path variable and query param."""
    return {"item_id": item_id, "q": q}

Then you can run the app:

uvicorn main:app --reload

Check out the FastAPI docs for examples of how to leverage Pydantic models to build these APIs. That’s all from me, until next time!