19/04/2023 - GO
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.
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())
}
package http
import (
"net/http"
)
func NewServer(handler http.Handler, address string) *http.Server {
return &http.Server{
Handler: handler,
Addr: address,
}
}
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))
}
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)
}
}
package http
type Error struct {
Status int
Code string
Message string
}
func (e Error) Error() string {
return e.Message
}
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
}
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",
}
)
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
}