Middleware
Handling cross-cutting, generic functionality
Middleware is a way to write reusable code that runs before or after (or both) the handling of API requests, often across several (or all) API endpoints.
It's commonly used to implement cross-cutting concerns like request logging, authentication, tracing, and so on. One of the benefits of Encore is that all of these use cases are already handled out-of-the-box, so there's no need to use middleware for those things.
Nonetheless, there are several use cases where it can be useful to write reusable functionality that applies to multiple API endpoints, and middleware is a good solution in those cases.
Encore provides built-in support for middleware by defining a function with the
//encore:middleware
directive. The middleware directive takes a target
parameter that specifies which API endpoints it applies to.
Middleware functions
A typical middleware implementation looks like this:
import (
"encore.dev/beta/errs"
"encore.dev/middleware"
)
//encore:middleware global target=all
func ValidationMiddleware(req middleware.Request, next middleware.Next) middleware.Response {
// If the payload has a Validate method, use it to validate the request.
payload := req.Data().Payload
if validator, ok := payload.(interface { Validate() error }); ok {
if err := validator.Validate(); err != nil {
// If the validation fails, return an InvalidArgument error.
err = errs.WrapCode(err, errs.InvalidArgument, "validation failed")
return middleware.Response{Err: err}
}
}
return next(req)
}
Middleware forms a chain, allowing each middleware to introspect and process
the incoming request before handing it off to the next middleware by calling the
next
function that's passed in as an argument. For the last middleware in the
chain, calling next
results in the actual API handler being called.
The req
parameter provides information about the incoming request
(see package docs).
The next
function returns a middleware.Response
object which contains the response from the API, describing whether there was an error, and on success
the actual response payload.
This enables middleware to also introspect and even modify the outgoing response, like this:
//encore:middleware target=tag:cache
func CachingMiddleware(req middleware.Request, next middleware.Next) middleware.Response {
data := req.Data()
// Check if we have the response cached. Use the request path as the cache key.
cacheKey := data.Path
if cached, err := loadFromCache(cacheKey, data.API.ResponseType); err == nil && cached != nil {
return middleware.Response{Payload: cached}
}
// Otherwise forward the request to the handler
return next(req)
}
This uses target=tag:cache
to have the middleware only apply to APIs that have
that tag. More on this below in Targeting APIs.
Take care
Middleware functions can also be defined as methods on a Dependency Injection
struct declared with //encore:service
. For example:
//encore:service
type Service struct{}
//encore:middleware target=all
func (s *Service) MyMiddleware(req middleware.Request, next middleware.Next) middleware.Response {
// ...
}
See the Dependency Injection docs for more information.
Middleware ordering
Middleware can either be defined inside a service, in which case it only runs
for APIs within that service, or it can be defined as a global
middleware,
in which case it applies to all services. For global middleware the target
directive still applies and enables you to easily match a subset of APIs.
Take care
Global middleware always run before all service-specific middleware, and then run in the order they are defined in the source code based on file name lexicographic ordering.
To avoid surprises it's best to define all middleware in a file called
middleware.go
in each service, and to create a single top-level package
to contain all global middleware.
Targeting APIs
The target
directive can either be provided as target=all
(meaning it applies
to all APIs) or a list of tags, in the form target=tag:foo,tag:bar
. Note that
these tags are evaluated with OR
, meaning the middleware applies to an API if
the API has at least one of those tags.
APIs can be defined with tags by adding tag:foo
at the end of the //encore:api
directive:
//encore:api public method=GET path=/user/:id tag:cache
func GetUser(ctx context.Context, id string) (*User, error) {
// ...
}