RESTful APIs
Writing a simple API using mostly vanilla go.
The file structure is as follows:
├── go.mod
├── handler
│ └── album.go
├── rest
│ └── rest.go
├── db
│ └── psql.go
└── server.go
The handler/album.go looks like following:
package handler
import (
"net/http"
"wdgfs/db"
"wdgfs/rest"
)
type Album struct {
ID int64 `json:"id"`
Title string `json:"title"`
Artist string `json:"artist"`
Price float32 `json:"price"`
}
func (p *Album) Index(r *http.Request) (any, error) {
return db.Query[Album](r.Context(), `select * from album`)
}
func (p *Album) Create(r *http.Request) (any, error) {
body := r.Context().Value(rest.Body).(Album)
return db.Exec(r.Context(),
`insert into album (title, artist, price) values ($1, $2, $3)`,
body.Title, body.Artist, body.Price)
}
func (p *Album) Retrive(r *http.Request, id string) (any, error) {
return db.QueryRow[Album](r.Context(), `select * from album where id = $1`, id)
}
func (p *Album) Update(r *http.Request, id string) (any, error) {
body := r.Context().Value(rest.Body).(Album)
return db.Exec(r.Context(), `update album set title=$1, artist=$2, price=$3`,
body.Title, body.Artist, body.Price)
}
func (p *Album) Delete(r *http.Request, id string) (any, error) {
return db.Exec(r.Context(), `delete from album where id=$1`, id)
}
A simple DB query layer is provided in db/psql.go:
package db
import (
"context"
"os"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)
var dbpool *pgxpool.Pool
func GetPool(ctx context.Context) (*pgxpool.Pool, error) {
if dbpool == nil {
var err error
dbpool, err = pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
if err != nil {
return dbpool, err
}
}
return dbpool, nil
}
func Query[T any](ctx context.Context, sql string, params ...any) ([]T, error) {
if _, err := GetPool(ctx); err != nil {
return nil, err
}
rows, err := dbpool.Query(ctx, sql, params...)
if err != nil {
return nil, err
}
return pgx.CollectRows(rows, pgx.RowToStructByName[T])
}
func QueryRow[T any](ctx context.Context, sql string, params ...any) (T, error) {
var t T
if _, err := GetPool(ctx); err != nil {
return t, err
}
rows, err := dbpool.Query(ctx, sql, params...)
if err != nil {
return t, err
}
return pgx.CollectOneRow(rows, pgx.RowToStructByName[T])
}
func Exec(ctx context.Context, sql string, params ...any) (pgconn.CommandTag, error) {
if _, err := GetPool(ctx); err != nil {
return pgconn.CommandTag{}, err
}
return dbpool.Exec(ctx, sql, params...)
}
REST style routing is done in rest/rest.go:
package rest
import (
"context"
"encoding/json"
"net/http"
)
type HttpError struct {
Code int
Message string
}
func (err HttpError) Error() string {
return err.Message
}
type RestfulResource interface {
Index(r *http.Request) (any, error)
Create(r *http.Request) (any, error)
Retrive(r *http.Request, id string) (any, error)
Update(r *http.Request, id string) (any, error)
Delete(r *http.Request, id string) (any, error)
}
type RestfulController[T any] struct {
Resource RestfulResource
}
type Handler func(r *http.Request) (any, error)
func HandleError(handler Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data, err := handler(r)
if err != nil {
if e, ok := err.(HttpError); ok {
http.Error(w, e.Message, e.Code)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}
}
type ContextKey struct{}
var Body = ContextKey{}
func ParseBody[T any](handler Handler) Handler {
return func(r *http.Request) (any, error) {
var body T
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return nil, err
}
return handler(r.WithContext(
context.WithValue(r.Context(), Body, body)))
}
}
type HandlerWithId func(r *http.Request, id string) (any, error)
func WithId(handler HandlerWithId) Handler {
return func(r *http.Request) (any, error) {
return handler(r, r.PathValue("id"))
}
}
func (c *RestfulController[T]) Mount(path string) *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("POST "+path, HandleError(ParseBody[T](c.Resource.Create)))
mux.HandleFunc("GET "+path+"{id}", HandleError(WithId(c.Resource.Retrive)))
mux.HandleFunc("PUT "+path+"{id}", HandleError(ParseBody[T](WithId(c.Resource.Update))))
mux.HandleFunc("DELETE "+path+"{id}", HandleError(WithId(c.Resource.Delete)))
mux.HandleFunc("GET "+path, HandleError(c.Resource.Index))
return mux
}
Finally connect them all together in server.go:
package main
import (
"net/http"
"wdgfs/handler"
"wdgfs/rest"
)
func main() {
http.Handle("/albums/", (&rest.RestfulController[handler.Album]{
Resource: &handler.Album{},
}).Mount("/albums/"))
if err := http.ListenAndServe(":3000", nil); err != nil {
panic(err)
}
}