rest

package module
v1.0.0 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 will need to provide an implementation of the Repository interface and a function to create such repository (the constructor). 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 receive a constructor for your repository, and an optional implementation of the Logger interface (compatible with Logrus). If no Logger is specified, the functions falls back to the default Go log package.

Example using Gorilla Pat:

	func NewThingsRepository(ctx context) rest.Repository {
		return &ThingsRepository{ctx: ctx}
	}

	func main() {
		router := pat.New()

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

		http.Handle("/", router)

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

Example using chi router:

	func main() {
		router := chi.NewRouter()

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

		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 Admin-on-rest package using pure Go, but can be used in other scenarios where you need a simple REST server for your data.

To use it, you will need to provide an implementation of the Repository interface and a function to create such repository.

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 receive a function used to construct your repository, and an optional implementation of Logger (compatible with Logrus). If no Logger is specified, the functions falls back to the default Go log package

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

func NewThingsRepository(ctx context) rest.Repository {
	return &ThingsRepository{ctx: ctx}
}

func main() {
	router := pat.New()

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

	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() {
	router := chi.NewRouter()

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

	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
admin-on-rest: https://marmelab.com/admin-on-rest/

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