In this example we are going to create an interface for HTTP handlers to standardise its use. Handlers will return a response object and error. It will help simplify error and response handling for API calls. It has been kept at minimal but you should improve it so don't take it as-is.


main.go


package main

import (
"log"

"random/http"
"random/users"
)

func main() {
rtr := http.NewRouter()
rtr.Add("/api/v1/users", users.Handler)

srv := http.NewServer(rtr.Handler, ":3000")
log.Fatal(srv.ListenAndServe())
}

http/server.go


package http

import (
"net/http"
)

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


http/router.go


package http

import "net/http"

type Router struct {
Handler *http.ServeMux
}

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

func (r *Router) Add(path string, hnd handler) {
r.Handler.Handle(path, handler(hnd))
}

http/handler.go


package http

import (
"errors"
"fmt"
"net/http"
)

type handler func(http.ResponseWriter, *http.Request) (*Response, error)

func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
res, err := h(w, r)
if err == nil {
h.success(w, res)
return
}

h.error(w, err)
}

func (h handler) success(w http.ResponseWriter, res *Response) {
if err := res.Write(w); err != nil {
log.Println(err)
}
}

func (h handler) error(w http.ResponseWriter, err error) {
var e Error
if errors.As(err, &e) {
res := &Response{
Status: e.Status,
Code: e.Code,
Message: e.Message,
}
if err := res.Write(w); err != nil {
log.Println(err)
}

return
}

log.Println(err)

res := &Response{
Status: http.StatusInternalServerError,
Code: "internal_error",
Message: "An internal error has occurred",
}
if err := res.Write(w); err != nil {
log.Println(err)
}
}

http/error.go


package http

type Error struct {
Status int
Code string
Message string
}

func (e Error) Error() string {
return e.Message
}

http/response.go


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

type Response struct {
Status int `json:"-"`
Headers map[string]string `json:"-"`

Data any `json:"data,omitempty"`
Meta any `json:"meta,omitempty"`

Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Errors map[string]string `json:"errors,omitempty"`
}

func (r *Response) Write(w http.ResponseWriter) error {
if r.Status < 200 || r.Status > 599 {
return fmt.Errorf("response: invalid status code: %d", r.Status)
}

for k, v := range r.Headers {
w.Header().Add(k, v)
}

if (r.Status >= 100 && r.Status <= 199) || r.Status == 204 || r.Status == 304 {
w.WriteHeader(r.Status)

return nil
}

bdy, err := json.Marshal(r)
if err != nil {
return fmt.Errorf("response: marshal body: %s", err.Error())
}

w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(r.Status)

if _, err := w.Write(bdy); err != nil {
return fmt.Errorf("response: write body: %s", err.Error())
}

return nil
}

domain/error.go


package domain

import (
"net/http"

httppkg "random/http"
)

var (
ErrInternalError = httppkg.Error{
Status: http.StatusInternalServerError,
Code: "internal_error",
Message: "An internal error has occurred",
}
ErrInvalidRequest = httppkg.Error{
Status: http.StatusBadRequest,
Code: "invalid_request",
Message: "Request is invalid",
}
ErrResourceConflict = httppkg.Error{
Status: http.StatusConflict,
Code: "resource_conflict",
Message: "A resource conflict has been detected",
}
ErrResourceNotFound = httppkg.Error{
Status: http.StatusNotFound,
Code: "resource_not_found",
Message: "Resource is not found",
}
)

users/api.go


package users

import (
"errors"
"net/http"

"random/domain"

httppkg "random/http"
)

func Handler(w http.ResponseWriter, r *http.Request) (*httppkg.Response, error) {
id := r.Header.Get("id")
if id == "" {
return nil, errors.New("not known error")
}
if id == "0" {
return nil, domain.ErrResourceNotFound
}
if id == "1" {
return nil, domain.ErrResourceConflict
}
if id == "3" {
return &httppkg.Response{
Status: domain.ErrInvalidRequest.Status,
Code: domain.ErrInvalidRequest.Code,
Message: domain.ErrInvalidRequest.Message,
Errors: map[string]string{"id": "Invalid digit"},
}, nil
}

return &httppkg.Response{
Status: http.StatusOK,
Data: "good job",
Meta: "paging",
}, nil
}