Astro JS Middleware Example: Request Logging
Rodney Lab
Posted on May 31, 2023
🚀 Astro JS Middleware Example
Astro now includes middleware, which performs a similar function to SvelteKit hooks, Deno Fresh middleware or middleware offered by serverless hosts (such as Cloudflare Workers). Here, we see an Astro JS Middleware example, as well as what middleware is and when you will pull it out of the Astro tool belt. For our example, we add request logging using Astro middleware with the Logtail serverless logging service.
At the time of writing, the functionality is still experimental, and the APIs may change. For that reason, check the current status before using Astro middleware in production projects. Also, bear in mind the APIs might evolve, so check the latest docs if you encounter issues (link further down the page).
What is Middleware?
Middleware is just code which runs on a server to intercept requests, potentially modifying them. Astro route files will be invoked, typically for specific routes (/docs/getting-started
, for example). Middleware is a little different in that respect, in that it can be less specific; you can invoke a middleware function for all routes, or selected sub-paths. With the logging example we consider, there will be some request details you want to capture, no matter which route the request was made to. Included here might be the request pathname, the time of the request, and perhaps even the time taken to respond. A single middleware file for all routes, rather than including the logging logic in the .astro
file for every route, makes sense here.
We will see within your middleware function, you can peek at what the response will be, then send that response, “as-is” or even change certain parts, adding HTTP headers or even changing out parts of the response body, where that makes sense. We also see Astro lets you pass through data to .astro
files using the locals object, offering another way to update content. Although Astro middleware works on both SSR and static routes, you will get the most out of it on SSR sites.
🤔 Why use Middleware?
Some uses for middleware are:
- analytics code;
- logging;
- performance and reliability measurement;
- manipulating HTML content (e.g. highlighting search terms in body text);
- adding response headers (such as HTTP security headers); and
- proxying requests, for example to keep a WordPress backend URL private.
🧱 What are we Building?
We will look at code for a basic Astro JS middleware logging example. In the code we shall see how you add a middleware function in Astro, as well as the most important methods, for intercepting requests. We also see how you can leave the response intact, yet still pass data through, for use in your front end code.
If that sounds like what you were looking for, then why don’t we get started? We won’t build an app from scratch, so, you probably want to create a feature branch on an existing project. If this is your first time working in Astro, though, see the quick guide on how to spin up a new Astro project.
🛠️ Enabling Astro Middleware
Because middleware is an experimental feature, at the time of writing, you need to enable it manually by updating astro.config.mjs
:
import { defineConfig } from 'astro/config';
import cloudflare from "@astrojs/cloudflare";
// https://astro.build/config
export default defineConfig({
experimental: {
middleware: true
},
output: "server",
adapter: cloudflare()
});
Middleware works on static as well as SSR sites, however you get most benefit on an SSR site, so we set the output
and adapter
fields in lines (10
& 11
), above. Drop these if you want to stay static. And, of course, switch out the Cloudflare adapter if you prefer using another host.
🕵🏽 Astro JS Middleware: Middleware Code
To add Astro middleware to all routes, just create a src/middleware.ts
(or src/middleware.js
) file which exports an onRequest
handler:
import type { MiddlewareHandler } from 'astro';
export const onRequest: MiddlewareHandler<Response> = async function onRequest(
{ locals, request },
next,
) {
const { url } = request;
const { pathname } = new URL(url);
const timestamp = new Date().toISOString();
(locals as { timestamp: string }).timestamp = timestamp;
const response = await next();
return response;
};
Some interesting points here:
-
onRequest
has two arguments: a context object andnext
-
request
on the context object is the incoming HTTP request from which you can destructure a URL, as well as other fields, which you will be familiar with -
locals
(on the context object) lets us pass out data from middleware -
next
is a function; calling that function gives you access to the response that the route normally generates (without middleware)
-
-
onRequest
returns a response object; in this case it is just the response which would have been sent anyway, though you can modify the response, adding HTTP headers or tinkering with the body
We also set locals.timestamp
, above. We will see that value is passed through to the front end code a little later.
Astro JS Middleware Example: Adding Logging
For our example, we want to log the request to our serverless logging service. Since we are just reporting, onRequest
can just return the response which would have been sent anyway (no need to modify the response).
Here is the extra code for logging a few response details, using Logtail:
import type { MiddlewareHandler } from 'astro';
import { pack } from 'msgpackr';
const logtailSourceToken = import.meta.env.LOGTAIL_SOURCE_TOKEN;
export const onRequest: MiddlewareHandler<Response> = async function onRequest(
{ locals, request },
next,
) {
const { url } = request;
const { pathname } = new URL(url);
const timestamp = new Date().toISOString();
(locals as { timestamp: string }).timestamp = timestamp;
const logData = {
dt: timestamp,
level: 'info',
message: 'astro-middleware-log',
item: {
pathname,
},
};
await fetch('https://in.logtail.com', {
method: 'POST',
headers: {
Authorization: `Bearer ${logtailSourceToken}`,
'Content-Type': 'application/msgpack',
},
body: pack(logData),
});
const response = await next();
return response;
};
Logtail has a REST API, and we can send the log data using fetch
; notice fetch is simply available in line 25
; no need to import it. You might notice, from the Content-Type
header, that the message body is in msgpack format. This is a slightly more efficient format than JSON. To encode the logData
regular JavaScript object in msgpack format, we use the msgpackr
package, imported in line 2
. The final piece to note is that you will need to add your LOGTAIL_SOURCE_TOKEN
to your .env
file, so that you can import it in line 4
.
🖥️ Astro JS Middleware Example: Frontend
You can update the response in your middleware, perhaps replacing template parameters. We might use this technique to vary content by region or time of day, and can do so just by altering the returned response from within the middleware. Another way of adding new data in middleware is using the locals
object. In the middleware code, we added the timestamp there:
locals.timestamp = timestamp;
We can access that value in individual .astro
files. Here is an example:
---
const { timestamp } = Astro.locals as { timestamp: string };
---
<html lang="en-GB">
<head> <!-- TRUNCATED... -->
</head>
<body>
<main class="container">
<section class="content">{timestamp}</section>
</main>
</body>
</html>
In line 2
, we pull our timestamp
off Astro.locals
. Note, this could be any arbitrary value we decided to include in the middleware function, though it should be possible to serialize the value as JSON. Finally, that value is now available for use in markup, as in line 11
.
Interestingly, middleware works on static as well as SSR sites. For a static site, though, the value displayed here would be the time stamp at build time, rather than at the point the request was made. This is still useful, though, for example to include build time in a page footer.
🙌🏽 Astro JS Middleware Example: Wrapping Up
In this post, we saw an example of using the Astro middleware API. In particular, we saw:
- how to add middleware to your Astro site;
- how you might use Astro middleware for request logging; and
- using Astro.locals to pass out data from middleware for use in markup.
You can see the full code for this project in the Rodney Lab GitHub repo. Also see the Astro docs in case there have been some changes in this experimental API, since writing. I do hope you have found this post useful! I am keen to hear how you will middleware in your next projects. Also, let me know about any possible improvements to the content above.
🙏🏽 Astro JS Middleware Example: Feedback
Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also, if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, please consider supporting me through Buy me a Coffee.
Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter, @rodney@toot.community on Mastodon and also the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Astro as well as SEO. Also, subscribe to the newsletter to keep up-to-date with our latest projects.
Posted on May 31, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.