Middlewares
In this chapter, you'll learn about middlewares and how to create them.
What is a Middleware?#
A middleware is a function executed when a request is sent to an API route. It's executed before the route handler function.
Middlewares are used to guard API routes, parse request content types other than application/json, manipulate request data, and more.
The HTTP server of the MedusaGo application is built to behave like the Express server that Medusa uses: it has an ordered stack of middlewares and route handlers, and a middleware calls a next function to pass the request to the next element in the stack.
Middleware Types#
There are two types of middlewares:
Type | Description | Example |
|---|---|---|
Global Middleware | A middleware that applies to all routes matching a specified pattern. |
|
Route Middleware | A middleware that applies to routes matching a specified pattern and HTTP method(s). | A middleware that applies to all |
These middlewares generally have the same definition and usage, but they differ in the routes they apply to. You'll learn how to create both types in the following sections.
How to Create a Middleware?#
A middleware is a function of type MiddlewareFunction from the framework/src/http package. It receives the request, the response, and the next function, and returns an error.
For example, this middleware logs a message for every request it runs for:
// logRequests logs every request it runs for, then calls the next
// middleware of the stack.
func logRequests(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse, next medusahttp.NextFunction) error {
slog.Info("Received a request!", "method", req.Method, "path", req.Path())
next(nil)
return nil
}
You apply middlewares to API routes with MiddlewareRoute values, which you pass in the Middlewares option of the CLI, in the same place you register your API routes.
For example:
// MiddlewaresExample shows the two types of middlewares.
var MiddlewaresExample = []medusahttp.MiddlewareRoute{
// A global middleware: it applies to every method.
{
Matcher: "/custom*",
Middlewares: []medusahttp.MiddlewareFunction{logRequests},
},
// A route middleware: it only applies to POST and PUT requests.
{
Matcher: "/custom*",
Methods: []string{"POST", "PUT"},
Middlewares: []medusahttp.MiddlewareFunction{logRequests},
},
}
A MiddlewareRoute has the following properties:
Matcher: The API route path to apply the middleware on, as a string. The string can include path parameters, such as/custom/:id, and the wildcard*, such as/custom*.MatcherRe: A regular expression to use instead ofMatcher. It's a Go*regexp.Regexp, matched against the request path.Middlewares: A slice of middleware functions to run.Methods: (optional) By default, a middleware is applied on all HTTP methods for a route. You can specify one or more HTTP methods to apply the middleware to in this option, making it a route middleware.BodyParser: (optional) Configures how the request body is parsed for the routes that match. Learn more in the Configure the Body Parser section.
Test the Middleware#
To test the middleware:
- Start the application:
- Send a request to any API route starting with
/custom. If you specified an HTTP method in theMethodsproperty, make sure to use that method. - See the log message
Received a request!in the terminal.
Apply a Middleware Using a Regular Expression#
You can set the MatcherRe property to a regular expression to apply a middleware to routes matching a pattern.
A regular expression must account for the full request path, including the leading slash. Go's regular expressions match anywhere in the path unless you anchor them, so anchor the pattern with ^ and $:
// RegexpMiddleware applies to /custom and every path starting with /custom/.
var RegexpMiddleware = medusahttp.MiddlewareRoute{
MatcherRe: regexp.MustCompile(`^/custom(/.*)?$`),
Middlewares: []medusahttp.MiddlewareFunction{logRequests},
}
This middleware applies to /custom and any route starting with /custom/. A pattern such as /custom/ without anchors would also match /other/custom/route.
When to Use Middlewares#
Middlewares are useful for:
- Protecting API routes to ensure that only authenticated users can access them.
- Validating request query and body parameters.
- Parsing request content types other than
application/json, or changing how the body is parsed. - Applying CORS configurations to custom API routes.
Middleware Function Parameters#
The middleware function accepts three parameters:
- A request object of type
*MedusaRequest. - A response object of type
*MedusaResponse. - A function of type
NextFunctionthat executes the next middleware in the stack.
next function in the middleware, with nil as its argument, unless you send a response or return an error. Otherwise, other middlewares and the API route handler won't execute.For example, the middleware in the first example logs the request's method and path, then calls next(nil) and returns nil to execute the next middleware in the stack.
Stop a Request#
A middleware can stop a request in two ways:
- Send a response with the
resobject, and don't callnext. - Return an error, or pass it to
next. The remaining middlewares and the route handler don't execute, and the application's error handler sends the error response.
For example, this middleware rejects requests that don't have the x-api-version header:
// requireApiVersion stops requests that don't send the header
// x-api-version: the route handler doesn't run.
func requireApiVersion(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse, next medusahttp.NextFunction) error {
if req.Header.Get("x-api-version") == "" {
// Returning an error (or passing it to next) skips the remaining
// middlewares and the route handler, and reaches the error handler.
return common.NewMedusaError(
common.MedusaErrorTypes.INVALID_DATA,
"The x-api-version header is required.",
)
}
next(nil)
return nil
}
Share Data with the Route Handler#
Use the Locals property of the request to pass values from a middleware to the next middlewares and the route handler. It's a map[string]any that's created for every request.
Middleware for Routes with Path Parameters#
To indicate a path parameter in a middleware's Matcher pattern, use the format :{param-name}.
For example:
// PathParamMiddleware applies to the routes of /custom/:id.
var PathParamMiddleware = medusahttp.MiddlewareRoute{
Matcher: "/custom/:id",
Middlewares: []medusahttp.MiddlewareFunction{logRequests},
}
This applies a middleware to the routes with the path /custom/:id.
/custom/.How Are Middlewares Ordered and Applied?#
Middleware and Routes Execution Order#
The MedusaGo application registers middlewares and API route handlers in the following order, stacking them on top of each other:
- Global middlewares in the following order:
- Global middlewares defined in the Medusa core.
- Global middlewares defined in the plugins (in the order the plugins are registered in).
- Global middlewares you define in the application.
- Route middlewares in the following order:
- Route middlewares defined in the Medusa core.
- Route middlewares defined in the plugins (in the order the plugins are registered in).
- Route middlewares you define in the application.
- API routes in the following order:
- API routes defined in the Medusa core.
- API routes defined in the plugins (in the order the plugins are registered in).
- API routes you define in the application.
Then, when a request is sent to an API route, the stack is executed in order: global middlewares are executed first, then the route middlewares, and finally the route handlers.
For example, consider you have the following middlewares:
// ExecutionOrderExample has a global middleware and a route middleware for
// the same path.
var ExecutionOrderExample = []medusahttp.MiddlewareRoute{
{
Matcher: "/custom",
Middlewares: []medusahttp.MiddlewareFunction{globalMiddleware},
},
{
Matcher: "/custom",
Methods: []string{"GET"},
Middlewares: []medusahttp.MiddlewareFunction{routeMiddleware},
},
}
When you send a GET request to the /custom route, globalMiddleware runs first, then routeMiddleware, and finally the route handler.
Middlewares Sorting#
On top of the previous ordering, MedusaGo sorts global and route middlewares based on their matcher pattern in the following order:
- Wildcard matchers. For example,
/custom*. - Regex matchers. For example,
/custom/(products|collections), and everyMatcherRe. - Static matchers without parameters. For example,
/custom. - Static matchers with parameters. For example,
/custom/:id.
For example, if you have the following middlewares:
// SortingExample has middlewares with different matchers.
var SortingExample = []medusahttp.MiddlewareRoute{
{Matcher: "/custom/:id", Middlewares: []medusahttp.MiddlewareFunction{a}},
{Matcher: "/custom", Middlewares: []medusahttp.MiddlewareFunction{b}},
{Matcher: "/custom*", Methods: []string{"GET"}, Middlewares: []medusahttp.MiddlewareFunction{c}},
{Matcher: "/custom/:id", Methods: []string{"GET"}, Middlewares: []medusahttp.MiddlewareFunction{d}},
}
The global middlewares are sorted into the following order before they're registered:
- Global middleware
b(/custom). - Global middleware
a(/custom/:id).
And the route middlewares are sorted into the following order before they're registered:
- Route middleware
c(/custom*). - Route middleware
d(/custom/:id).
Then, the middlewares are registered in the order mentioned earlier, with global middlewares first, then the route middlewares.
Configure the Body Parser#
The application parses the request body before the middlewares run, depending on the request's Content-Type header: application/json bodies, application/x-www-form-urlencoded bodies, and text/plain bodies. The result is in req.Body.
To change how the body of requests to specific routes is parsed, set the BodyParser property of a MiddlewareRoute. It's a pointer to a BodyParserConfig, which has the following properties:
SizeLimit: The maximum size of the request body, as a string such as"500kb"or"2mb". The default is 100 kilobytes. If a request's body is larger, the application responds with a413status code.PreserveRawBody: Whether to keep the bytes of the body inreq.RawBody, in addition to the parsed body.Disabled: Turns the body parser off.req.Bodyisn't set, and the handler can read the stream fromreq.Request.Body.
For example, this configuration keeps the raw body of requests sent to a webhook route, to verify the signature that the sender computed from the exact bytes it sent:
// verifySignature computes the HMAC of the exact bytes that were received.
func verifySignature(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse, next medusahttp.NextFunction) error {
mac := hmac.New(sha256.New, []byte("secret"))
mac.Write(req.RawBody)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(req.Header.Get("x-signature"))) {
return common.NewMedusaError(common.MedusaErrorTypes.UNAUTHORIZED, "Invalid signature.")
}
next(nil)
return nil
}
// WebhookMiddlewares keep the raw request body and raise the size limit of
// POST /webhooks/custom.
var WebhookMiddlewares = []medusahttp.MiddlewareRoute{
{
Matcher: "/webhooks/custom",
Methods: []string{"POST"},
BodyParser: &medusahttp.BodyParserConfig{
SizeLimit: "500kb",
PreserveRawBody: true,
},
Middlewares: []medusahttp.MiddlewareFunction{verifySignature},
},
}
The route handler can then use the body as usual:
func webhookPOST(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
return res.JSON(map[string]any{"received": len(req.RawBody)})
}
To disable the body parser for a route, set Disabled:
// RawStreamMiddlewares turn the body parser off, so the handler reads
// req.Request.Body itself.
var RawStreamMiddlewares = []medusahttp.MiddlewareRoute{
{
Matcher: "/webhooks/stream",
Methods: []string{"POST"},
BodyParser: &medusahttp.BodyParserConfig{Disabled: true},
},
}
BodyParser configuration applies to the routes that match its Matcher and Methods, whether or not the MiddlewareRoute has middlewares.Overriding Middlewares#
A middleware can not override an existing middleware. Instead, middlewares are added to the end of the middleware stack.
For example, if you define a custom validation middleware, such as ValidateAndTransformBody, on an existing route, then both the original and the custom validation middleware will run.
Similarly, if you add an authenticate middleware to an existing route, both the original and the custom authentication middleware will run. So, you can't override the original authentication middleware.
Alternative Solution to Overriding Middlewares#
If you need to change the middlewares applied to a route, you can create a custom API route that executes the same functionality as the original route, but with the middlewares you want.