Astro JS Middleware Example: Request Logging

Rodney Lab - May 31 '23 - - Dev Community

🚀 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.

Astro JS Middleware Example: Screen capture shows an event logged in the Log tail console. Message reads astro-middleware-log. Timestamp and pathname are included, as, well as the event level, which is set to info.

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()
});
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

Some interesting points here:

  • onRequest has two arguments: a context object and next
    • 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;
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

Astro JS Middleware Example: Screen capture shows a browser window with a timestamp in large text.

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .