Go 1.22 introduced an enhanced version of the net/http package's router. This includes method matching and wildcards. In this example we are going to create a custom HTTP package for it. The package will also allow us to use global and route specific middlewares as bonus point.


Files


main.go


package main

import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"

"rest/pkg/http"
"rest/pkg/middleware"
"rest/src/api"
)

func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

// HTTP api
user := api.User{}

// HTTP router
rtr := http.NewRouter()
rtr.Use(middleware.AccessLogger)
rtr.Add("POST /api/v1/users", user.Create, middleware.DeprecatedRoute)
rtr.Add("DELETE /api/v1/users/{id}", user.Delete)

// HTTP server
srv := http.NewServer(rtr, ":1234")
go func() {
log.Println("api running ...")
if err := srv.Start(); err != nil {
log.Fatalln(err)
}
}()
if err := srv.Shutdown(ctx, time.Second*5); err != nil {
log.Println(err)
}
log.Println("api shutdown ...")
}

router.go


package http

import (
"net/http"
"slices"
)

type Router struct {
handler *http.ServeMux
middlewares []func(http.Handler) http.Handler
}

func NewRouter() *Router {
return &Router{
handler: http.NewServeMux(),
}
}

func (r *Router) Use(middlewares ...func(http.Handler) http.Handler) {
r.middlewares = middlewares
}

func (r *Router) Add(path string, handler http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) {
mids := slices.Concat(r.middlewares, middlewares)

for i := len(mids) - 1; i >= 0; i-- {
handler = mids[i](handler).(http.HandlerFunc)
}

r.handler.HandleFunc(path, handler)
}

server.go


package http

import (
"context"
"net/http"
"time"
)

type Server struct {
server *http.Server
}

func NewServer(router *Router, address string) Server {
return Server{
server: &http.Server{
Handler: router.handler,
Addr: address,
},
}
}

func (s Server) Start() error {
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}

return nil
}

func (s Server) Shutdown(ctx context.Context, timeout time.Duration) error {
<-ctx.Done()

ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

if err := s.server.Shutdown(ctx); err != nil {
return err
}

return nil
}

access_logger.go


package middleware

import (
"log"
"net/http"
)

func AccessLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method + " " + r.RequestURI)

next.ServeHTTP(w, r)
})
}

deprecated_route.go


package middleware

import (
"log"
"net/http"
)

func DeprecatedRoute(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("this route has been deprecated")

w.WriteHeader(http.StatusGone)
})
}

user.go


package api

import (
"log"
"net/http"
)

type User struct{}

func (u User) Create(w http.ResponseWriter, r *http.Request) {
log.Println("user create")
}

func (u User) Delete(w http.ResponseWriter, r *http.Request) {
log.Println("user delete", r.PathValue("id"))
}

Test


$ curl -iX DELETE "http://localhost:1234/api/v1/users/1"

2024/06/04 19:56:52 DELETE /api/v1/users/1
2024/06/04 19:56:52 user delete 1

$ curl -iX POST "http://localhost:1234/api/v1/users" -d '{"key":"val"}'

2024/06/04 19:56:55 POST /api/v1/users
2024/06/04 19:56:55 this route has been deprecated