Astro has two incredibly powerful features: middleware and API endpoints.
In short, API Endpoints would be useful when you want a route to perform as an actual backend endpoint:
export function get() {
return {
body: JSON.stringify({
now: Date.now(),
}),
};
}
When we do a GET to this endpoint, we will get a JSON response:
{"now":1688891051814}
Middlewares are awesome to perform in any server task that would run before your application code would be executed.
export function onRequest ({ locals, request }, next) {
// intercept response data from a request
// optionally, transform the response by modifying `locals`
locals.title = "New title";
// return a Response or the result of calling `next()`
return next();
};
As you can see, we can call the next() function or return an actual Response object, and that's when the problems start.
Problem 1: Simple return error
In the example above, when trying to perform the GET request to that endpoint, we will get the following error message:
[middleware] Using simple endpoints can cause unexpected issues in the chain of middleware functions.
It's strongly suggested to use full Response objects.
error Any data returned from middleware must be a valid `Response` object.
This means that because now the middleware is processing the request using next() and our endpoint is returning an object, it happens a mismatch between the expected output.
To fix that, we have to change our endpoint response to return a proper Response:
export function get() {
const body = JSON.stringify({
now: Date.now(),
}); // body must be a string
return new Response(body);
}
Now, the middleware "next()" code will be executed correctly.
Problem 2: redirect logic
A common use case for middleware is for handling languages.
For example, my website runs in both English and Portuguese. When someone hits the base URL without the language, my middleware will determine, based on the request headers where, what's the base language.
Here's some sudo code:
export const onRequest: MiddlewareResponseHandler = async (
{ request, redirect },
next
) => {
const url = new URL(request.url);
const isMissingLocale = checkMissingLocale(url.pathname);
if (isMissingLocale) {
const locale = getLanguageFromAcceptLanguage(
request.headers.get(`accept-language`) || `en`
);
const urlWithLocale = getUrlWithLocale(url.pathname, locale);
return redirect(urlWithLocale);
}
return next();
};
That would work fine, except this logic applies to the API endpoint.
Endpoints will likely never have translations, so we need to bypass the middleware.
To do that, we could allowlist a few endpoints and check when the middleware runs. If it's present in this list, then we don't do any logic:
export const onRequest: MiddlewareResponseHandler = async (
{ request, redirect },
next
) => {
const url = new URL(request.url);
if (skipMiddleware(url.pathname)) {
return next();
}
const isMissingLocale = checkMissingLocale(url.pathname);
if (isMissingLocale) {
const locale = getLanguageFromAcceptLanguage(
request.headers.get(`accept-language`) || `en`
);
const urlWithLocale = getUrlWithLocale(url.pathname, locale);
return redirect(urlWithLocale);
}
return next();
};
const passthroughRoutes = [`/api`];
function skipMiddleware(urlPathname: string) {
let shouldSkip = false;
for (const route of passthroughRoutes) {
if (urlPathname.startsWith(route)) {
shouldSkip = true;
break;
}
}
return shouldSkip;
}
Before running the logic, we'll first check whether the incoming request must follow those rules.
Remember that if you're using the sequence object, you'll probably need to add the same logic to all functions that need to be bypassed.