README
¶
REST with Clean Architecture for Go
This module implements HTTP transport level for github.com/swaggest/usecase
to build REST services.
Goals
- Maintain single source of truth for documentation, validation and input/output of HTTP API.
- Avoid dependency on compile time code generation.
- Improve productivity and reliability by abstracting HTTP details with simple API for common case.
- Allow low-level customizations for advanced cases.
- Maintain reasonable performance with low GC impact.
Non-Goals
- Support for legacy documentation schemas like Swagger 2.0 or RAML.
- Zero allocations.
- Explicit support for XML in request or response bodies.
Features
- Compatible with
net/http
. - Built with
github.com/go-chi/chi
router. - Modular flexible structure.
- HTTP request mapping into Go value based on field tags.
- Decoupled business logic with Clean Architecture use cases.
- Automatic type-safe OpenAPI 3.0/3.1 documentation with
github.com/swaggest/openapi-go
. - Single source of truth for the documentation and endpoint interface.
- Automatic request/response JSON schema validation with
github.com/santhosh-tekuri/jsonschema
. - Dynamic gzip compression and fast pass through mode.
- Optimized performance.
- Embedded Swagger UI.
- Generic interface for use case interactors.
Usage
Please check this tutorial for end-to-end usage example.
Request Decoder
Go struct with field tags defines input port.
Request decoder populates field values from http.Request
data before use case interactor is invoked.
// Declare input port type.
type helloInput struct {
Locale string `query:"locale" default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$" enum:"ru-RU,en-US"`
Name string `path:"name" minLength:"3"` // Field tags define parameter location and JSON schema constraints.
// Field tags of unnamed fields are applied to parent schema,
// they are optional and can be used to disallow unknown parameters.
// For non-body params, name tag must be provided explicitly.
// E.g. here no unknown `query` and `cookie` parameters allowed,
// unknown `header` params are ok.
_ struct{} `query:"_" cookie:"_" additionalProperties:"false"`
}
Input data can be located in:
path
parameter in request URI, e.g./users/{name}
,query
parameter in request URI, e.g./users?locale=en-US
,formData
parameter in request body withapplication/x-www-form-urlencoded
ormultipart/form-data
content,form
parameter acts asformData
orquery
,json
parameter in request body withapplication/json
content,cookie
parameter in request cookie,header
parameter in request header.
For more explicit separation of concerns between use case and transport it is possible to provide request mapping
separately when initializing handler (please note, such mapping is not applied to json
body).
// Declare input port type.
type helloInput struct {
Locale string `default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$"`
Name string `minLength:"3"` // Field tags define parameter location and JSON schema constraints.
}
// Add use case handler with custom input mapping to router.
r.Method(http.MethodGet, "/hello/{name}", nethttp.NewHandler(u,
nethttp.RequestMapping(new(struct {
Locale string `query:"locale"`
Name string `path:"name"` // Field tags define parameter location and JSON schema constraints.
})),
))
Additional field tags describe JSON schema constraints, please check documentation.
More schema customizations are possible with github.com/swaggest/jsonschema-go interfaces
.
By default default
tags are only contributing to documentation,
if request.DecoderFactory.ApplyDefaults
is
set to true
, fields of request structure that don't have explicit value but have default
will be populated with
default value.
If input structure implements request.Loader
,
then LoadFromHTTPRequest(r *http.Request) error
method will be invoked to populate input structure instead
of automatic decoding. This allows low level control for cases that need it.
Request decoder can be used standalone, in already existing `ServeHTTP`.
type MyRequest struct {
Foo int `header:"X-Foo"`
Bar string `formData:"bar"`
Baz bool `query:"baz"`
}
// A decoder for particular structure, can be reused for multiple HTTP requests.
myDecoder := request.NewDecoderFactory().MakeDecoder(http.MethodPost, new(MyRequest), nil)
// Request and response writer from ServeHTTP.
var (
rw http.ResponseWriter
req *http.Request
)
// This code would presumably live in ServeHTTP.
var myReq MyRequest
if err := myDecoder.Decode(req, &myReq, nil); err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
}
Response Encoder
Go struct with field tags defines output port.
Response encoder writes data from output to http.ResponseWriter
after use case interactor invocation finishes.
// Declare output port type.
type helloOutput struct {
Now time.Time `header:"X-Now" json:"-"`
Message string `json:"message"`
Sess string `cookie:"sess,httponly,secure,max-age=86400,samesite=lax"`
}
Output data can be located in:
json
for response body withapplication/json
content,header
for values in response header,cookie
for cookie values, cookie fields can have configuration in field tag (same as in actual cookie, but with comma separation).
For more explicit separation of concerns between use case and transport it is possible to provide response header mapping separately when initializing handler.
// Declare output port type.
type helloOutput struct {
Now time.Time `json:"-"`
Message string `json:"message"`
}
// Add use case handler with custom output headers mapping to router.
r.Method(http.MethodGet, "/hello/{name}", nethttp.NewHandler(u,
nethttp.ResponseHeaderMapping(new(struct {
Now time.Time `header:"X-Now"`
})),
))
Additional field tags describe JSON schema constraints, please check documentation.
Creating Use Case Interactor
HTTP transport is decoupled from business logic by adapting use case interactors.
Use case interactor can define input and output ports that are used to map data between Go values and transport. It can provide information about itself that will be exposed in generated documentation.
// Create use case interactor with references to input/output types and interaction function.
u := usecase.NewInteractor(func(ctx context.Context, input helloInput, output *helloOutput) error {
msg, available := messages[input.Locale]
if !available {
return status.Wrap(errors.New("unknown locale"), status.InvalidArgument)
}
output.Message = fmt.Sprintf(msg, input.Name)
output.Now = time.Now()
return nil
})
Initializing Web Service
Web Service is an instrumented facade in front of router, it simplifies configuration and provides more compact API to add use cases.
// Service initializes router with required middlewares.
service := web.NewService(openapi31.NewReflector())
// It allows OpenAPI configuration.
service.OpenAPISchema().SetTitle("Albums API")
service.OpenAPISchema().SetDescription("This service provides API to manage albums.")
service.OpenAPISchema().SetVersion("v1.0.0")
// Additional middlewares can be added.
service.Use(
middleware.StripSlashes,
// cors.AllowAll().Handler, // "github.com/rs/cors", 3rd-party CORS middleware can also be configured here.
)
// Use cases can be mounted using short syntax .<Method>(...).
service.Post("/albums", postAlbums(), nethttp.SuccessStatus(http.StatusCreated))
log.Println("Starting service at http://localhost:8080")
if err := http.ListenAndServe("localhost:8080", service); err != nil {
log.Fatal(err)
}
Usually, web.Service
API is sufficient, but if it is not, router can be configured manually, please check
the documentation below.
Security Setup
Example with HTTP Basic Auth.
// Prepare middleware with suitable security schema.
// It will perform actual security check for every relevant request.
adminAuth := middleware.BasicAuth("Admin Access", map[string]string{"admin": "admin"})
// Prepare API schema updater middleware.
// It will annotate handler documentation with security schema.
adminSecuritySchema := nethttp.HTTPBasicSecurityMiddleware(apiSchema, "Admin", "Admin access")
// Endpoints with admin access.
r.Route("/admin", func(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(adminAuth, adminSecuritySchema) // Add both middlewares to routing group to enforce and document security.
r.Method(http.MethodPut, "/hello/{name}", nethttp.NewHandler(u))
})
})
Example with cookie.
// Security middlewares.
// - sessMW is the actual request-level processor,
// - sessDoc is a handler-level wrapper to expose docs.
sessMW := func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("sessid"); err == nil {
r = r.WithContext(context.WithValue(r.Context(), "sessionID", c.Value))
}
handler.ServeHTTP(w, r)
})
}
sessDoc := nethttp.APIKeySecurityMiddleware(s.OpenAPICollector, "User",
"sessid", oapi.InCookie, "Session cookie.")
// Security schema is configured for a single top-level route.
s.With(sessMW, sessDoc).Method(http.MethodGet, "/root-with-session", nethttp.NewHandler(dummy()))
// Security schema is configured on a sub-router.
s.Route("/deeper-with-session", func(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(sessMW, sessDoc)
r.Method(http.MethodGet, "/one", nethttp.NewHandler(dummy()))
r.Method(http.MethodGet, "/two", nethttp.NewHandler(dummy()))
})
})
See example.
Handler Setup
Handler is a generalized adapter for use case interactor, so usually setup is trivial.
// Add use case handler to router.
r.Method(http.MethodGet, "/hello/{name}", nethttp.NewHandler(u))
Example
For non-generic use case, see another example.
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"time"
"github.com/swaggest/openapi-go/openapi31"
"github.com/swaggest/rest/response/gzip"
"github.com/swaggest/rest/web"
swgui "github.com/swaggest/swgui/v5emb"
"github.com/swaggest/usecase"
"github.com/swaggest/usecase/status"
)
func main() {
s := web.NewService(openapi31.NewReflector())
// Init API documentation schema.
s.OpenAPISchema().SetTitle("Basic Example")
s.OpenAPISchema().SetDescription("This app showcases a trivial REST API.")
s.OpenAPISchema().SetVersion("v1.2.3")
// Setup middlewares.
s.Wrap(
gzip.Middleware, // Response compression with support for direct gzip pass through.
)
// Declare input port type.
type helloInput struct {
Locale string `query:"locale" default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$" enum:"ru-RU,en-US"`
Name string `path:"name" minLength:"3"` // Field tags define parameter location and JSON schema constraints.
// Field tags of unnamed fields are applied to parent schema.
// they are optional and can be used to disallow unknown parameters.
// For non-body params, name tag must be provided explicitly.
// E.g. here no unknown `query` and `cookie` parameters allowed,
// unknown `header` params are ok.
_ struct{} `query:"_" cookie:"_" additionalProperties:"false"`
}
// Declare output port type.
type helloOutput struct {
Now time.Time `header:"X-Now" json:"-"`
Message string `json:"message"`
}
messages := map[string]string{
"en-US": "Hello, %s!",
"ru-RU": "Привет, %s!",
}
// Create use case interactor with references to input/output types and interaction function.
u := usecase.NewInteractor(func(ctx context.Context, input helloInput, output *helloOutput) error {
msg, available := messages[input.Locale]
if !available {
return status.Wrap(errors.New("unknown locale"), status.InvalidArgument)
}
output.Message = fmt.Sprintf(msg, input.Name)
output.Now = time.Now()
return nil
})
// Describe use case interactor.
u.SetTitle("Greeter")
u.SetDescription("Greeter greets you.")
u.SetExpectedErrors(status.InvalidArgument)
// Add use case handler to router.
s.Get("/hello/{name}", u)
// Swagger UI endpoint at /docs.
s.Docs("/docs", swgui.New)
// Start server.
log.Println("http://localhost:8011/docs")
if err := http.ListenAndServe("localhost:8011", s); err != nil {
log.Fatal(err)
}
}
Additional Integrations
Performance Optimization
If top performance is critical for the service or particular endpoints, you can trade simplicity for performance by implementing manual request loader on input type.
func (i *myInput) LoadFromHTTPRequest(r *http.Request) (err error) {
i.Header = r.Header.Get("X-Header")
return nil
}
If request.Loader
is implemented, it will be called instead of both automatic decoding and validation.
Check advanced example.
To further improve performance you may try to use fasthttp
instead of net/http
with
rest-fasthttp
fork.
Versioning
This project adheres to Semantic Versioning.
Before version 1.0.0
, breaking changes are tagged with MINOR
bump, features and fixes are tagged with PATCH
bump.
After version 1.0.0
, breaking changes are tagged with MAJOR
bump.
Breaking changes are described in UPGRADE.md.
Advanced Usage
Documentation
¶
Overview ¶
Package rest provides http handler for use case interactor to implement REST API.
Index ¶
- Constants
- func HTTPStatusFromCanonicalCode(c status.Code) int
- func OutputHasNoContent(output interface{}) bool
- type ETagged
- type ErrResponse
- type ErrWithAppCode
- type ErrWithCanonicalStatus
- type ErrWithFields
- type ErrWithHTTPStatus
- type HTTPCodeAsError
- type HandlerTrait
- type HandlerWithRoute
- type HandlerWithUseCase
- type JSONSchemaValidator
- type JSONWriterTo
- type OutputWithHTTPStatus
- type ParamIn
- type RequestErrors
- type RequestJSONSchemaProvider
- type RequestMapping
- type RequestValidatorFactory
- type ResponseJSONSchemaProvider
- type ResponseValidatorFactory
- type ValidationErrors
- type Validator
- type ValidatorFunc
Constants ¶
const ( // ParamInPath indicates path parameters, such as `/users/{id}`. ParamInPath = ParamIn("path") // ParamInQuery indicates query parameters, such as `/users?page=10`. ParamInQuery = ParamIn("query") // ParamInBody indicates body value, such as `{"id": 10}`. ParamInBody = ParamIn("body") // ParamInFormData indicates body form parameters. ParamInFormData = ParamIn("formData") // ParamInCookie indicates cookie parameters, which are passed ParamIn the `Cookie` header, // such as `Cookie: debug=0; gdpr=2`. ParamInCookie = ParamIn("cookie") // ParamInHeader indicates header parameters, such as `X-Header: value`. ParamInHeader = ParamIn("header") )
Variables ¶
This section is empty.
Functions ¶
func HTTPStatusFromCanonicalCode ¶
HTTPStatusFromCanonicalCode returns http status accordingly to use case status code.
func OutputHasNoContent ¶
func OutputHasNoContent(output interface{}) bool
OutputHasNoContent indicates if output does not seem to have any content body to render in response.
Types ¶
type ETagged ¶
type ETagged interface {
ETag() string
}
ETagged exposes specific version of resource.
type ErrResponse ¶
type ErrResponse struct { StatusText string `json:"status,omitempty" description:"Status text."` AppCode int `json:"code,omitempty" description:"Application-specific error code."` ErrorText string `json:"error,omitempty" description:"Error message."` Context map[string]interface{} `json:"context,omitempty" description:"Application context."` // contains filtered or unexported fields }
ErrResponse is HTTP error response body.
func Err ¶
func Err(err error) (int, ErrResponse)
Err creates HTTP status code and ErrResponse for error.
You can use it with use case status code:
rest.Err(status.NotFound)
type ErrWithAppCode ¶
ErrWithAppCode exposes application error code.
type ErrWithCanonicalStatus ¶
ErrWithCanonicalStatus exposes canonical status code.
type ErrWithFields ¶
ErrWithFields exposes structured context of error.
type ErrWithHTTPStatus ¶
ErrWithHTTPStatus exposes HTTP status code.
type HTTPCodeAsError ¶ added in v0.2.43
type HTTPCodeAsError int
HTTPCodeAsError exposes HTTP status code as use case error that can be translated to response status.
func (HTTPCodeAsError) Error ¶ added in v0.2.43
func (c HTTPCodeAsError) Error() string
Error return HTTP status text.
func (HTTPCodeAsError) HTTPStatus ¶ added in v0.2.43
func (c HTTPCodeAsError) HTTPStatus() int
HTTPStatus returns HTTP status code.
type HandlerTrait ¶
type HandlerTrait struct { // SuccessStatus is an HTTP status code to set on successful use case interaction. // // Default is 200 (OK) or 204 (No Content). SuccessStatus int // SuccessContentType is a Content-Type of successful response, default application/json. SuccessContentType string // MakeErrResp overrides error response builder instead of default Err, // returned values are HTTP status code and error structure to be marshaled. MakeErrResp func(ctx context.Context, err error) (int, interface{}) // ReqMapping controls request decoding into use case input. // Optional, if not set field tags are used as mapping. ReqMapping RequestMapping RespHeaderMapping map[string]string RespCookieMapping map[string]http.Cookie // ReqValidator validates decoded request data. ReqValidator Validator // RespValidator validates decoded response data. RespValidator Validator // OperationAnnotations are called after operation setup and before adding operation to documentation. // // Deprecated: use OpenAPIAnnotations. OperationAnnotations []func(op *openapi3.Operation) error // OpenAPIAnnotations are called after operation setup and before adding operation to documentation. OpenAPIAnnotations []func(oc openapi.OperationContext) error }
HandlerTrait controls basic behavior of rest handler.
func (*HandlerTrait) RequestMapping ¶
func (h *HandlerTrait) RequestMapping() RequestMapping
RequestMapping returns custom mapping for request decoder.
func (*HandlerTrait) RestHandler ¶
func (h *HandlerTrait) RestHandler() *HandlerTrait
RestHandler is an accessor.
type HandlerWithRoute ¶
type HandlerWithRoute interface { // RouteMethod returns http method of action. RouteMethod() string // RoutePattern returns http path pattern of action. RoutePattern() string }
HandlerWithRoute is a http.Handler with routing information.
type HandlerWithUseCase ¶
type HandlerWithUseCase interface {
UseCase() usecase.Interactor
}
HandlerWithUseCase exposes usecase.
type JSONSchemaValidator ¶
type JSONSchemaValidator interface { Validator // AddSchema accepts JSON schema for a request parameter or response value. AddSchema(in ParamIn, name string, schemaData []byte, required bool) error }
JSONSchemaValidator defines JSON schema validator.
type JSONWriterTo ¶
JSONWriterTo writes JSON payload.
type OutputWithHTTPStatus ¶ added in v0.2.47
OutputWithHTTPStatus exposes HTTP status code(s) for output.
type RequestErrors ¶
RequestErrors is a list of validation or decoding errors.
Key is field position (e.g. "path:id" or "body"), value is a list of issues with the field.
func (RequestErrors) Fields ¶
func (re RequestErrors) Fields() map[string]interface{}
Fields returns request errors by field location and name.
type RequestJSONSchemaProvider ¶
type RequestJSONSchemaProvider interface { ProvideRequestJSONSchemas( method string, input interface{}, mapping RequestMapping, validator JSONSchemaValidator, ) error }
RequestJSONSchemaProvider provides request JSON Schemas.
type RequestMapping ¶
RequestMapping describes how decoded request should be applied to container struct.
It is defined as a map by parameter location. Each item is a map with struct field name as key and decoded field name as value.
Example:
map[rest.ParamIn]map[string]string{rest.ParamInQuery:map[string]string{"ID": "id", "FirstName": "first-name"}}
type RequestValidatorFactory ¶
type RequestValidatorFactory interface {
MakeRequestValidator(method string, input interface{}, mapping RequestMapping) Validator
}
RequestValidatorFactory creates request validator for particular structured Go input value.
type ResponseJSONSchemaProvider ¶
type ResponseJSONSchemaProvider interface { ProvideResponseJSONSchemas( statusCode int, contentType string, output interface{}, headerMapping map[string]string, validator JSONSchemaValidator, ) error }
ResponseJSONSchemaProvider provides response JSON Schemas.
type ResponseValidatorFactory ¶
type ResponseValidatorFactory interface { MakeResponseValidator( statusCode int, contentType string, output interface{}, headerMapping map[string]string, ) Validator }
ResponseValidatorFactory creates response validator for particular structured Go output value.
type ValidationErrors ¶
ValidationErrors is a list of validation errors.
Key is field position (e.g. "path:id" or "body"), value is a list of issues with the field.
func (ValidationErrors) Error ¶
func (re ValidationErrors) Error() string
Error returns error message.
func (ValidationErrors) Fields ¶
func (re ValidationErrors) Fields() map[string]interface{}
Fields returns request errors by field location and name.
type Validator ¶
type Validator interface { // ValidateData validates decoded request/response data and returns error in case of invalid data. ValidateData(in ParamIn, namedData map[string]interface{}) error // ValidateJSONBody validates JSON encoded body and returns error in case of invalid data. ValidateJSONBody(jsonBody []byte) error // HasConstraints indicates if there are validation rules for parameter location. HasConstraints(in ParamIn) bool }
Validator validates a map of decoded data.
type ValidatorFunc ¶
ValidatorFunc implements Validator with a func.
func (ValidatorFunc) HasConstraints ¶ added in v0.1.3
func (v ValidatorFunc) HasConstraints(_ ParamIn) bool
HasConstraints indicates if there are validation rules for parameter location.
func (ValidatorFunc) ValidateData ¶
func (v ValidatorFunc) ValidateData(in ParamIn, namedData map[string]interface{}) error
ValidateData implements Validator.
func (ValidatorFunc) ValidateJSONBody ¶
func (v ValidatorFunc) ValidateJSONBody(body []byte) error
ValidateJSONBody implements Validator.
Source Files
¶
Directories
¶
Path | Synopsis |
---|---|
_examples
module
|
|
Package chirouter provides instrumentation for chi.Router.
|
Package chirouter provides instrumentation for chi.Router. |
Package gorillamux provides OpenAPI docs collector for gorilla/mux web services.
|
Package gorillamux provides OpenAPI docs collector for gorilla/mux web services. |
Package gzip provides pre-compressed data container.
|
Package gzip provides pre-compressed data container. |
Package jsonschema implements request validator with github.com/santhosh-tekuri/jsonschema/v2.
|
Package jsonschema implements request validator with github.com/santhosh-tekuri/jsonschema/v2. |
Package nethttp provides instrumentation for net/http.
|
Package nethttp provides instrumentation for net/http. |
Package openapi provides documentation collector.
|
Package openapi provides documentation collector. |
Package request implements reflection-based net/http request decoder.
|
Package request implements reflection-based net/http request decoder. |
Package response implements reflection-based net/http response encoder.
|
Package response implements reflection-based net/http response encoder. |
gzip
Package gzip provides http compression support.
|
Package gzip provides http compression support. |
Package resttest provides utilities to test REST API.
|
Package resttest provides utilities to test REST API. |
Package web provides default facades for web service bootstrap.
|
Package web provides default facades for web service bootstrap. |