REST (easy) framework in Go with out of the box OpenAPI generation, validation, dependency injection, and much more.
- Dependency Injection How values are sent to middleware and endpoint functions.
- Router Defining the routes, middlware, & documentation.
- Middleware Code that runs before it reaches the final endpoint
- Inspection How types are converted into documentation.
- Validation How to control validation.
- Documentation All the ways to specify documentation.
- Site The main site type and its useful methods.
type Echo struct {
Message string `json:"message" api:"desc=A message to echo."`
}
site := rez.New(chi.NewRouter())
// /echo?message=HelloWorld!
site.Get("/echo", func(q rez.Query[Echo]) (*Echo, *rez.NotFound[string]) {
if q.Value.Message == "" {
return nil, rez.NewNotFound("message is required")
}
return &q.Value, nil
})
site.ServeSwaggerUI("/doc/swagger", nil)
site.ServeRedoc("/doc/redoc")
site.Listen(":3000")
More can be found in examples.
Dependency injection is used to pass arguments to a middleware or route functions. rez uses deps for dependency injection. There are several types that get injected out of the box:
context.Context
: The context of the request.deps.Scope
: The scope which holds all the given values that can be injected for the current request. The rootSite
type has a parent scope which request scopes can inherit values from.http.ResponseWriter
: The outgoing response.http.Request
: The incoming request.rez.Router
: The reference to the router where the middleware was used or the route was defined on.rez.Path[P]
: A generic wrapper which holds the struct that is parsed from the path parameters. If the path is/task/{taskID}
and the struct istype TaskPath struct { TaskID int }
theTaskID
property will be populated from the value in the URL.rez.Query[Q]
: A generic wrapper which holds the struct that is parsed from the query string. If the url is?message=Hi×=4
and the struct istype MyQuery struct { Message string, Times int }
the Message and Times fields will be populated from the query string.rez.Header[H]
: A generic wrapper which holds the struct that is parsed from the headers.rez.Body[B]
: A generic wrapper which holds the type that is parsed from the request body.rez.Request[B, P, Q]
: A generic wrapper which holds the body, params, and query structs that are to be parsed from the request.rez.Validator
: A validator for the route or middleware.api.Operation
: The operation (route only).rez.MiddlewareNext
: Invoke the next handler (middleware only).
There are a few other methods to get other injectable values.
- Use
rez.Site.Scope
to set global values and providers. - Implement
rez.Injectable
. - Use
rez.Router.DefineBody(bodies...)
to define types that will only be used as arguments that should come from the request body. - Use
rez.Router.DefinePath(paths...)
to define types that will only be used as arguments that should come from the request path parameters. - Use
rez.Router.DefineQuery(queries...)
to define types that will only be used as arguments that should come from the request query parameters. - Use
rez.Router.DefineHeader(headers...)
to define types that will only be used as arguments that should come from the request headers. - Use
*deps.Scope
as an argument in middleware andSet
orProvide
other values that the following handlers will be able to receive.
The rez.Router is a wrapper of chi.Router where instead of http.Hander
s and http.HandlerFunc
you pass in a func(args) results
which gets its arguments injected, and in the case of middleware is able to provide injected values for routes in the router. The function argument and result types are also inspected to build the OpenAPI documentation.
Middleware in rez is also a dependency injected function. The middleware can return nothing or can return an error which if non-nil will be sent as the response. The middleware has a special injected value rez.MiddlewareNext
which is a function to call if we want to call the next handler. Any arguments or return types that are identified as headers, queries, paths, request bodies, or responses are added as those objects in all routes that are in the router using the middleware.
Example:
// site.Use(authMiddleware)
func authMiddleware(next rez.MiddlewareNext, r *http.Request) *rez.Unauthorized[string] {
// Accessing headers this way doesn't add it as a parameter to all routes that use the middleware.
auth := r.Header.Get("Authorization")
if auth == "" {
return rez.NewUnauthorized("No access")
}
next()
return nil
}
You can also pass down injectable values to all middlewares and routes which are defined after middleware by setting the value on the scope.
type User struct { ID int }
// site.Use(authMiddleware)
func authMiddleware(next rez.MiddlewareNext, s *deps.Scope) {
// authenticate user and return error, if successful apply the user to the scope.
s.Set(User{ID: 23})
next()
}
type Task struct { ID int, Name string, Done bool }
// set.Get("/tasks", getTasks)
func getTasks(user User) Task[] {
// get tasks the user can see, we only get here if authMiddleware called next
return []Task{}
}
As mentioned what you reference with middleware could add to the operations that follow. This middleware is an example of authentication where the token is foolishly sent in the query string. All operations that follow will have the specified security scheme, accept token
as a query parameter, and could respond with rez.Unauthorized[string]
.
type Auth struct { Token string }
type AuthMiddleware func(s *deps.Scope, next rez.MiddlewareNext, q rez.Query[Auth]) *rez.Unauthorized[string]
// All routes which use this middleware accept this type of security
func (auth AuthMiddleware) APIOperationUpdate(op *api.Operation) {
op.Security = append(op.Security, map[string][]string{"queryAuth": {}})
}
var authMiddleware AuthMiddleware = func(s *deps.Scope, next rez.MiddlewareNext, q rez.Query[Auth]) *rez.Unauthorized[string] {
if q.Value.Token == "" {
return rez.NewUnauthorized("No access")
}
s.Set(q.Value)
next()
return nil
}
func echoToken(token Auth) Auth {
return token
}
// Usage
site.Open.AddSecurity("queryAuth", &api.Security{
Type: api.SecurityTypeApiKey,
Name: "token",
In: api.ParameterInQuery,
})
site.Use(authMiddleware)
site.Get("/token", echoToken)
Function arguments are inspected to determine what path parameters, query parameters, headers, and body is used by a route. See Dependency Injection for more details on that. The types detected are converted into api
objects and are added to the OpenAPI document and referenced in the path & operations in the path. The function return arguments are inspected for possible responses - most of the time these return types will be pointers for routes which can have multiple response types (or no specific response type). If the return type implements rez.HasStatus
that is where the status code is pulled from. If the return type does not it's assumed to be a possible OK (200) result. The schemas built from the argument and return types are built once and can be controlled using various functions and interfaces. If the type is a struct then json
and api
tags can control the field visibility or schema options. See Documentation for additional details on how to control the documentation & validation that is generated.
Validation in rez is done if enabled and only for certain schema fields and after the data is marshalled into values. So any invalid type errors will not be triggered by the validation but when the JSON is parsed. General validation options can be applied per type, validation can be enabled or disabled for any router, and types can have custom validation code that takes over the validation process or runs after the validation process. If validation fails the error is returned to the user. How those validations are sent to the user can be controlled by calling rez.Router.SetErrorHandler
.
rez.Router.EnabledValidation(bool)
enables or disables validation in this router and any sub-routers created after this call. By default validation is not enabled.rez.Router.SetValidationOptions(any,ValidationOptions)
sets the validation options for the given type, which controls if validation is skipped, if format is enforced, or if specifying deprecated values triggers a validation error.rez.CanValidateFull
if a type implements this it handles all validation logic.rez.CanValidatePost
if a type implements this it will do additional validation logic after other validation logic has been done.rez.Injectable
if a type implements this it must implement anAPIValidate
method.
The following schema fields are used during validation:
MultipleOf
,Maximum
,Minimum
,ExclusiveMaximum
,ExclusiveMinimum
are used for any int or float types.MaxLength
,MinLength
are used for string types.Deprecated
,Nullable
,Pattern
,Format
,Enum
,OneOf
,AllOf
,AnyOf
,Not
are used for all types.MinItems
,MaxItems
,Items
,UniqueItems
are used for array and slice types.MinProperties
,MaxProperties
,AdditionalProperties
are used for map types.Properties
,Required
are used for struct types.
Documentation is control by various ways on the types themselves or through router methods.
api.HasName
A type's documented name is the name of the type in the GO code, but there might be collisions. If there are collisions the OpenAPI built will have schema names that include the types pkg path to be unique. To avoid those potentially lengthy names you can implementapi.HasName
like so:
// tasks folder
type Search struct { Name string }
func (Search) APIName() string { return "TaskSearch" }
api.Description
A request or response's description can be specified on tyhe type by implementing this interface. Using the code above.
func (Search) APIDescription() string { return "This is used to determine what Tasks to return." }
api.HasBaseSchema
A type's schema will be dynamically determined, but implementing this interface will provide the schema building logic with a starting point. You can define the preferred schema properties.
func (Search) APIBaseSchema() *api.Schema {
return &api.Schema{
Title: "Task Search",
Description: "This is used to determine what Tasks to return.",
Example api.Any(Search{Name: "homework"}),
}
}
api.HasFullSchema
A type's schema will only be determined by what's returned, no further inspection is done. This is useful if you want to use some of the built-in struct types that support different formats.
type Timestamp time.Time
func (Timestamp) APIFullSchema() *api.Schema {
return &api.Schema{
Type: api.DataTypeString,
Format: "date-time",
Pattern: `\\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d+\d\d:\d\d`,
Example: api.Any("2018-11-13T20:20:39+00:00"),
}
}
api.HasEnum
A type can accept only a handful of values.
type TodoAction string
const (
TodoActionArchive TodoAction = "archive"
TodoActionDelete TodoAction = "delete"
TodoActionComplete TodoAction = "complete"
)
func (TodoAction) APIEnum() []any {
return []any{TodoActionArchive, TodoActionDelete, TodoActionComplete}
}
api.HasExamples
A type can provide several named examples for a given content type.
func (Search) APIExamples(contentType api.ContentType) api.Examples {
return api.Examples{
"All tasks": api.Example{
Summary: "This search will return all tasks the user can see.",
Value: api.Any(Search{}),
},
"Tasks with 'homework' in the name": api.Example{
Summary: "This search will return all tasks with 'homework' in the name.",
Value: api.Any(Search{Name: "homework"}),
},
}
}
api.HasExample
A type that can provide a single example for a type.
func (Search) APIExample() *any {
return api.Any(Search{Name: "homework"})
}
api.HasOperation
A route function that has the operation fully defined here and no inspection needs to be done on the arguments or return types.
type GetTask func(id string) *Task
func (GetTask) APIOperation() api.Operation {
return api.Operation{
Tags: []string{"Task"},
Summary: "Get the task with the given ID",
OperationID: "GET_TASK_BY_ID",
Parameters: []api.Parameter{{
Name: "id",
In: api.ParameterInPath,
Required: true,
Schema: &api.Schema{Type: api.TypeString},
Example: api.Any("87y34"),
}},
Responses: api.Responses{
"200": &api.Response{
Description: "The task exists and has these values",
Content: api.Contents{
api.ContentTypeJSON: &api.MediaType{
Schema: site.Open.GetSchema(reflect.TypeOf(Task{})),
},
},
},
},
}
}
api.HasOperationUpdate
A route function that modifies the operation inspected after its done inspection.
type GetTask func(id string) *Task
func (GetTask) APIOperationUpdate(op *api.Operation) {
op.OperationID = "GET_TASK_BY_ID"
}
rez.Site.Open
is a reference toapi.Builder
which has aDocument
field which can be modified. This is the base document to use before building the finalapi.Document
.rez.Router
has a few methods to assist in documentation:GetOperations() *api.Operation
returns a reference to the operation template that has accumulated at this point in the router. Sub routers inherit this. Middlewares add to it.SetOperations(api.Operation)
sets the operation template in its entirety, overwriting what has been built so far.UpdateOperations(api.Operation)
merges in the fields set on the given operation into the operation template of this router.GetPath(pattern) *api.Path
returns a reference to the path with the given pattern. Defining methods will add operations to this path. If the path has not been defined yet nil is returned.CreatePath(pattern) *api.Path
returns a reference to the path with the given pattern, creating it if need be.UpdatePath(pattern, api.Path)
merges in the fields set on the given path with the path defined at the given pattern - creating it if need be.SetTags(tags []string)
sets the tags on the operation template to this value.AddTags(tags)
adds the tags to the operation template.SetResponses(api.Responses)
sets the responses for all operations defined after. This overwrites any responses specified previously by the user or middlewares.AddResponses(api.Responses)
adds the responses to the operation template.AddResponse(code, api.Response)
adds the response to the operation template.HandleFunc(pattern, fn, ...api.Operation) *api.Path
can accept zero or more operation definitions to merge into the operations defined at this path - and the reference to the path at the pattern is returned."method"(pattern, fn, ...api.Operation) *api.Operation
is a method with the name of any of the HTTP methods which adds this method to the path with the pattern and merges in any given operations with the operation template and then returns the reference to the final built operation for this route.
- Struct tags. Fields on a struct can specify the
api
tag which is a comma-delimited list of key=value or flags. If you need to use a comma in a value you can escape it like\,
.title
ex:api:"title=A person's address"
(seeapi.Schema.Title
)desc
ordescription
ex:api:"desc=The ten digit home phone number."
(seeapi.Schema.Description
)format
ex:api:"format=email"
(seeapi.Schema.Format
)pattern
ex:api:"pattern=\d+"
(seeapi.Schema.Pattern
)deprecated
ex:api:"deprecated"
(seeapi.Schema.Deprecated
)required
ex:api:"required"
(seeapi.Schema.Nullable
)null
ornullable
ex:api:"null"
(seeapi.Schema.Nullable
)readonly
ex:api:"readonly"
(seeapi.Schema.ReadOnly
)writeonly
ex:api:"writeonly"
(seeapi.Schema.WriteOnly
)enum
ex:api:"enum=1|2|3"
(seeapi.Schema.Enum
)minlength
ex:api:"minlength=6"
(seeapi.Schema.MinLength
)maxlength
ex:api:"maxlength=6"
(seeapi.Schema.MaxLength
)minitems
ex:api:"minitems=6"
(seeapi.Schema.MinItems
)maxitems
ex:api:"maxitems=6"
(seeapi.Schema.MaxItems
)multipleof
ex:api:"multipleof=2"
(seeapi.Schema.MultipleOf
)min
orminimum
ex:api:"min=1"
(seeapi.Schema.Minimum
)max
ormaximum
ex:api:"max=1"
(seeapi.Schema.Maximum
)exclusivemaximum
orexclusivemax
ex:api:"exclusivemax=true"
(seeapi.Schema.ExclusiveMaximum
)exclusiveminimum
orexclusivemin
ex:api:"exclusivemin"
(seeapi.Schema.ExclusiveMinimum
)
rez.Site
is the implementation of router that must be created with rez.New(chi.Router)
. Site has a few additional methods:
BuildDocument() *api.Document
returns the built document based on the routes and middlewares defined thus far.BuildJSON() []byte
callsBuildDocument
and marshals it to JSON.ServeOpenJSON(patten)
serves theBuildJSON
to a GET route at the defined pattern. This gets called by the otherServe
document related endpoints if it was not called yet with a default pattern ofopenapi3.json
.ServeSwaggerUI(pattern,options)
serves an HTML page at the given pattern which presents the SwaggerUI which points to the OpenAPI document JSON.ServeRedoc(pattern)
serves an HTML page at the given pattern which presents the Redoc which points to the OpenAPI document JSON.Listen(addr)
starts the site and blocks until it stops.Run()
starts the site but looks at the CLI args for a--host
argument to specify the port. It defaults to:80
.PrintPaths()
prints an ASCII grid to the console with the paths described in the site at this point in time. Includes the "Method", "URL", and "About" if any summary or descriptions are given. Example output:
┌───────┬───────────┬────────────────────┐
│Method │URL │About │
├───────┼───────────┼────────────────────┤
│GET │/task/{id} │Get task by id │
├───────┼───────────┼────────────────────┤
│DELETE │/task/{id} │Delete task by id │
├───────┼───────────┼────────────────────┤
│GET │/auth │Get current session │
├───────┼───────────┼────────────────────┤
│POST │/auth │Login │
├───────┼───────────┼────────────────────┤
│DELETE │/auth │Logout │
└───────┴───────────┴────────────────────┘