Building REST APIs With Go 1.22 http.ServeMux
Prior to Go 1.22, it has been very difficult to develop REST APIs in Go by simply using Go standard library packages. One primary reason for that difficulty is that the ServeMux (HTTP request multiplexer) type of net/http library doesn’t support to map URI patterns with HTTP verbs, which is very important to design RESTful resources. And thus, we eventually moved to using third-party router libraries like Gorilla mux, Go chi, etc for building REST APIs. Some developers have bee using frameworks like echo and gin. If you are not a fanatic to web frameworks — IMHO, web frameworks are not needed for simply building REST APIs , and would like to build REST APIs without using a third-party router libraries, you can make it with ServeMux type of net/http library introduced in Go 1.22.
Mapping HTTP Methods with ServeMux
With Go 1.22, ServeMux can now map the URI patterns with the HTTP methods. This capability has been achieved without breaking any API changes in the ServeMux type. The pattern parameter of Handle
and HandleFunc
methods are now look like the following:
[METHOD ][HOST]/[PATH]
Here, all three parts are optional. If HTTP method is present, it must be followed by a single space as the code block below:
Listing 1. Mapping URI patterns with ServeMux
mux := http.NewServeMux()
mux.HandleFunc("GET /api/notes", GetAll)
mux.HandleFunc("POST /api/notes", Post)
In the preceding code block, we put HTTP verbs, GET and POST before specifying the URI. If HTTP method is not specified, it will still be a valid configuration without mapping HTTP verbs.
Here’s an example routing configuration with ServeMux:
Listing 2. Configuring ServeMux router patterns with HTTP methods
func initializeRoutes(h *apphttp.NoteHandler) *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/notes", h.GetAll)
mux.HandleFunc("GET /api/notes/{id}", h.Get)
mux.HandleFunc("POST /api/notes", h.Post)
mux.HandleFunc("PUT /api/notes/{id}", h.Put)
mux.HandleFunc("DELETE /api/notes/{id}", h.Delete)
return mux
}
The route parameters like {id} can be retrieved by using the request.PathValue
method. The PathValue
method returns the value for the named path wildcard in the ServeMux pattern that matched the request. It returns the empty string if the request was not matched against a pattern or there is no such wildcard in the pattern.
Listing 3. Getting the value of a route parameter named id
// Getting route parameter id
id := r.PathValue("id")
The code blocks below provide the implementations for creating the HTTP server in package main and HTTP handler functions in a library package. The source code is available in Github from here.
Listing 4. Creating HTTP server in package main
package main
import (
"log"
"net/http"
apphttp "github.com/shijuvar/gokit/examples/http-api/httpmux"
"github.com/shijuvar/gokit/examples/http-api/memstore"
)
// Entry point of the program
func main() {
repo, err := memstore.NewInmemoryRepository() // With in-memory database
if err != nil {
log.Fatal("Error:", err)
}
h := &apphttp.NoteHandler{
Repository: repo, // Injecting dependency
}
router := initializeRoutes(h) // configure routes
server := &http.Server{
Addr: ":8080",
Handler: router,
}
log.Println("Listening...")
server.ListenAndServe() // Run the http server
}
func initializeRoutes(h *apphttp.NoteHandler) *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/notes", h.GetAll)
mux.HandleFunc("GET /api/notes/{id}", h.Get)
mux.HandleFunc("POST /api/notes", h.Post)
mux.HandleFunc("PUT /api/notes/{id}", h.Put)
mux.HandleFunc("DELETE /api/notes/{id}", h.Delete)
return mux
}
Listing 5. HTTP Handler functions for CRUD on a domain entity
package httpmux
import (
"encoding/json"
"errors"
"net/http"
"github.com/shijuvar/gokit/examples/http-api/model"
)
// NoteHandler organizes HTTP handler functions for CRUD on Note entity
type NoteHandler struct {
Repository model.Repository // interface for persistence
}
// Post handles HTTP Post - /api/notes
func (h *NoteHandler) Post(w http.ResponseWriter, r *http.Request) {
var note model.Note
// Decode the incoming note json
err := json.NewDecoder(r.Body).Decode(¬e)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Create note
if err := h.Repository.Create(note); err != nil {
if errors.Is(err, model.ErrNoteExists) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
// GetAll handles HTTP Get - /api/notes
func (h *NoteHandler) GetAll(w http.ResponseWriter, r *http.Request) {
// Get all
if notes, err := h.Repository.GetAll(); err != nil {
if errors.Is(err, model.ErrNotFound) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} else {
j, err := json.Marshal(notes)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(j)
}
}
// Get handles HTTP Get - /api/notes/{id}
func (h *NoteHandler) Get(w http.ResponseWriter, r *http.Request) {
// Getting route parameter id
id := r.PathValue("id")
// Get by id
if note, err := h.Repository.GetById(id); err != nil {
if errors.Is(err, model.ErrNotFound) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} else {
w.Header().Set("Content-Type", "application/json")
j, err := json.Marshal(note)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
w.Write(j)
}
}
// Put handles HTTP Put - /api/notes/{id}
func (h *NoteHandler) Put(w http.ResponseWriter, r *http.Request) {
// Getting route parameter id
id := r.PathValue("id")
var note model.Note
// Decode the incoming note json
err := json.NewDecoder(r.Body).Decode(¬e)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Update
if err := h.Repository.Update(id, note); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
// Delete handles HTTP Delete - /api/notes/{id}
func (h *NoteHandler) Delete(w http.ResponseWriter, r *http.Request) {
// Getting route parameter id
id := r.PathValue("id")
// delete
if err := h.Repository.Delete(id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}