rest

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 25, 2026 License: MIT Imports: 12 Imported by: 11

README

Simple Generic REST controller

Build Status Go Report Card GoDoc Maintainability Coverage

This package provides a simple REST controller compatible with the JSON Server API "dialect". This package enables the creation of backends for the great React-admin framework using pure Go, but can be used in other scenarios where you need a simple REST server for your data.

To use it, you need to provide an implementation of the generic Repository[T] interface for your entity type T. Implement Persistable[T] as well to enable the POST, PUT and DELETE methods; without it, those methods return 405 Method Not Allowed. Every repository method receives the request's context.Context as its first parameter. For a simple implementation of an in-memory repository, see /examples/sample_repository.go.

The controller was created to be used with Gorilla Pat, as it requires URL params to be parsed and set as query params. You can easily adapt it to work with other routers and frameworks using a custom middleware.

The functionality is provided by a set of handlers named after the REST verbs they handle: Get(), GetAll(), Put(), Post() and Delete(). Each of these functions receives your repository instance and returns an http.HandlerFunc.

Errors returned by your repository are matched with errors.Is / errors.As, so they can be wrapped:

Error HTTP status
rest.ErrNotFound 404, e.g. {"error":"Thing(id:1) not found"}
rest.ErrPermissionDenied 403
*rest.ValidationError 400, with the field errors in the body
any other error 500

Example using Gorilla Pat:

	type Thing struct {
		ID   string
		Name string
	}

	// ThingsRepository implements rest.Repository[Thing] and rest.Persistable[Thing]
	type ThingsRepository struct {
		// your storage, e.g. a *sql.DB
	}

	func main() {
		repo := rest.Repository[Thing](&ThingsRepository{})
		router := pat.New()

		router.Get("/thing/{id}", rest.Get(repo))
		router.Get("/thing", rest.GetAll(repo))
		router.Post("/thing", rest.Post(repo))
		router.Put("/thing/{id}", rest.Put(repo))
		router.Delete("/thing/{id}", rest.Delete(repo))

		http.Handle("/", router)

		log.Print("Listening on 127.0.0.1:8000...")
		log.Fatal(http.ListenAndServe(":8000", nil))
	}

Example using chi router:

	func main() {
		repo := rest.Repository[Thing](&ThingsRepository{})
		router := chi.NewRouter()

		router.Route("/thing", func(r chi.Router) {
			r.Get("/", rest.GetAll(repo))
			r.Post("/", rest.Post(repo))
			r.Route("/{id:[0-9]+}", func(r chi.Router) {
				r.With(urlParams).Get("/", rest.Get(repo))
				r.With(urlParams).Put("/", rest.Put(repo))
				r.With(urlParams).Delete("/", rest.Delete(repo))
			})
		})

		http.Handle("/", router)

		log.Print("Listening on 127.0.0.1:8000...")
		log.Fatal(http.ListenAndServe(":8000", nil))
	}

	// Middleware to convert Chi URL params (from Context) to query params, as expected by our REST package
	func urlParams(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx := chi.RouteContext(r.Context())
			parts := make([]string, 0)
			for i, key := range ctx.URLParams.Keys {
				value := ctx.URLParams.Values[i]
				if key == "*" {
					continue
				}
				parts = append(parts, url.QueryEscape(":"+key)+"="+url.QueryEscape(value))
			}
			q := strings.Join(parts, "&")
			if r.URL.RawQuery == "" {
				r.URL.RawQuery = q
			} else {
				r.URL.RawQuery += "&" + q
			}

			next.ServeHTTP(w, r)
		})
	}

Add an issue if you need examples for other routers/frameworks

Documentation

Overview

Package rest provides a simple REST controller compatible with the JSON Server API "dialect". This package enables the creation of backends for the great React-admin framework using pure Go, but can be used in other scenarios where you need a simple REST server for your data.

To use it, you need to provide an implementation of the generic Repository[T] interface for your entity type T. Implement Persistable[T] as well to enable the POST, PUT and DELETE methods; without it, those methods return 405 Method Not Allowed. Every repository method receives the request's context.Context as its first parameter.

The controller was created to be used with Gorilla Pat, as it requires URL params to be parsed and set as query params. You can easily adapt it to work with other routers and frameworks using a custom middleware.

The functionality is provided by a set of handlers named after the REST verbs they handle: Get(), GetAll(), Put(), Post() and Delete(). Each of these functions receives your repository instance and returns an http.HandlerFunc.

Errors returned by your repository are matched with errors.Is / errors.As, so they can be wrapped. ErrNotFound maps to 404 (for example {"error":"Thing(id:1) not found"}), ErrPermissionDenied to 403, a *ValidationError to 400 with the field errors in the body, and any other error to 500.

Example using Gorilla Pat (https://github.com/gorilla/pat):

type Thing struct {
	ID   string
	Name string
}

// ThingsRepository implements rest.Repository[Thing] and rest.Persistable[Thing]
type ThingsRepository struct {
	// your storage, e.g. a *sql.DB
}

func main() {
	repo := rest.Repository[Thing](&ThingsRepository{})
	router := pat.New()

	router.Get("/thing/{id}", rest.Get(repo))
	router.Get("/thing", rest.GetAll(repo))
	router.Post("/thing", rest.Post(repo))
	router.Put("/thing/{id}", rest.Put(repo))
	router.Delete("/thing/{id}", rest.Delete(repo))

	http.Handle("/", router)

	log.Print("Listening on 127.0.0.1:8000...")
	log.Fatal(http.ListenAndServe(":8000", nil))
}

Example using chi router (https://github.com/go-chi/chi):

func main() {
	repo := rest.Repository[Thing](&ThingsRepository{})
	router := chi.NewRouter()

	router.Route("/thing", func(r chi.Router) {
		r.Get("/", rest.GetAll(repo))
		r.Post("/", rest.Post(repo))
		r.Route("/{id:[0-9]+}", func(r chi.Router) {
			r.With(urlParams).Get("/", rest.Get(repo))
			r.With(urlParams).Put("/", rest.Put(repo))
			r.With(urlParams).Delete("/", rest.Delete(repo))
		})
	})

	http.Handle("/", router)

	log.Print("Listening on 127.0.0.1:8000...")
	log.Fatal(http.ListenAndServe(":8000", nil))
}

// Middleware to convert Chi URL params (from Context) to query params, as expected by our REST package
func urlParams(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := chi.RouteContext(r.Context())
		parts := make([]string, 0)
		for i, key := range ctx.URLParams.Keys {
			value := ctx.URLParams.Values[i]
			if key == "*" {
				continue
			}
			parts = append(parts, url.QueryEscape(":"+key)+"="+url.QueryEscape(value))
		}
		q := strings.Join(parts, "&")
		if r.URL.RawQuery == "" {
			r.URL.RawQuery = q
		} else {
			r.URL.RawQuery += "&" + q
		}

		next.ServeHTTP(w, r)
	})
}

For more info see:

JSON Server: https://github.com/typicode/json-server
React-admin: https://marmelab.com/react-admin/

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotFound will make the controller return a 404 error
	ErrNotFound = errors.New("data not found")

	// ErrPermissionDenied will make the controller return a 403 error
	ErrPermissionDenied = errors.New("permission denied")

	// ErrInvalidQueryOpts will make the controller return a 400 error
	ErrInvalidQueryOpts = errors.New("invalid query options")
)

Possible errors returned by a Repository implementation. Any error other than these will make the REST controller return a 500 http status code.

Functions

func Delete

func Delete[T any](repository Repository[T]) http.HandlerFunc

Delete handles the DELETE verb. Should be mapped to: DELETE /thing/:id

func Get

func Get[T any](repository Repository[T]) http.HandlerFunc

Get handles the GET verb for individual items. Should be mapped to: GET /thing/:id

func GetAll

func GetAll[T any](repository Repository[T]) http.HandlerFunc

GetAll handles the GET verb for the full collection. Should be mapped to: GET /thing For all query options available, see https://github.com/typicode/json-server

func Post

func Post[T any](repository Repository[T]) http.HandlerFunc

Post handles the POST verb. Should be mapped to: POST /thing

func Put

func Put[T any](repository Repository[T]) http.HandlerFunc

Put handles the PUT verb. Should be mapped to: PUT /thing/:id

func RespondWithError

func RespondWithError(w http.ResponseWriter, code int, message string) error

RespondWithError returns an error message formatted as a JSON object, and sets the http status to code

func RespondWithJSON

func RespondWithJSON(w http.ResponseWriter, code int, payload any) error

RespondWithJSON returns a message formatted as JSON, and sets the http status to code

Types

type Controller

type Controller[T any] struct {
	Repository Repository[T]
}

Controller implements a set of RESTful handlers, compatible with the JSON Server API "dialect". Please prefer to use the functions provided in the handler.go file instead of these.

func (*Controller[T]) Delete

func (c *Controller[T]) Delete(w http.ResponseWriter, r *http.Request)

Delete handles the DELETE verb

func (*Controller[T]) Get

func (c *Controller[T]) Get(w http.ResponseWriter, r *http.Request)

Get handles the GET verb for individual items.

func (*Controller[T]) GetAll

func (c *Controller[T]) GetAll(w http.ResponseWriter, r *http.Request)

GetAll handles the GET verb for the full collection

func (*Controller[T]) Post

func (c *Controller[T]) Post(w http.ResponseWriter, r *http.Request)

Post handles the POST verb

func (*Controller[T]) Put

func (c *Controller[T]) Put(w http.ResponseWriter, r *http.Request)

Put handles the PUT verb

type Persistable

type Persistable[T any] interface {
	// Save adds the entity to the repository and returns the newly created id
	Save(ctx context.Context, entity *T) (string, error)

	// Update the entity identified by id. Optionally select the fields to be updated
	Update(ctx context.Context, id string, entity T, fields ...string) error

	// Delete the entities identified by id
	Delete(ctx context.Context, ids ...string) error
}

Persistable must be implemented by repositories in addition to the Repository interface, to allow the POST, PUT and DELETE http methods. If this interface is not implemented by the repository, calls to these http methods will return 405 - Method Not Allowed.

type QueryOptions

type QueryOptions struct {
	// Comma separated list of fields to sort the data
	Sort string

	// Possible values: asc (default), desc
	Order string

	// Max records to return. Used for pagination
	Max int

	// Initial record to return. Used for pagination
	Offset int

	// Map of filters to apply to the query. Keys are field names and values are the filter
	// to be applied to the field. E.g.: {"age": 30, "name": "john"}.
	// How the values of the filters are applied to the fields is implementation dependent
	// (you can implement substring, exact match, etc...)
	Filters map[string]any
}

QueryOptions are optional query parameters that can be received by Count and ReadAll and are used to implement pagination, sorting and filtering.

type Repository

type Repository[T any] interface {
	// Count returns the number of entities that matches the criteria specified by the options
	Count(context.Context, ...QueryOptions) (int64, error)

	// Read returns the entity identified by id
	Read(ctx context.Context, id string) (*T, error)

	// ReadAll returns a slice of entities that matches the criteria specified by the options
	ReadAll(context.Context, ...QueryOptions) ([]T, error)
}

Repository is the interface that must be created to access your data. This will be used for the GET http method.

type ValidationError

type ValidationError struct {
	Errors map[string]string `json:"errors"`
}

ValidationError will make the controller return a 400 error, with the listed errors in the body

func (ValidationError) Error

func (m ValidationError) Error() string

func (ValidationError) Is

func (m ValidationError) Is(target error) bool

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL