Astro
Astro Middleware for Request Handling
Written by Noel
Published:
16 min read
Topics researched with AI assistance; reviewed and edited by Noel before publishing.

Explore this topic
More Astro guides, glossary entries, and practical workflows live on the topic hub.
Astro middleware is the request layer that runs before a page or endpoint finishes rendering. It lets you inspect the incoming request, attach request-specific data, and decide whether to continue, rewrite, or return a response early.
For merchants and developers, that matters because it centralizes logic that would otherwise be repeated across pages, layouts, or API routes. If a site needs consistent locale handling, auth checks, or request-aware content, middleware is the place to do it.
Key takeaways
- Middleware is most useful when the same request logic must apply across many routes.
context.localsis the shared handoff point between middleware, pages, and API routes.- Middleware can run at build time for prerendered pages and at request time for on-demand pages.
- Rewrites and early responses belong in middleware when routing decisions depend on request data.
- Type-safe locals reduce bugs because shared request data stays predictable across the app.
What is it?
Astro middleware is a function that intercepts a request before the final response is produced. In practical terms, it sits between the incoming request and the rendered page, giving you a place to run logic that should happen consistently for a route or a group of routes.
A simple example is a store that wants to show a different header when a visitor is signed in. Instead of checking session state in every page component, middleware can read the session once, store a user object in context.locals, and make that data available anywhere in the request lifecycle.
The important part is that middleware is not just for authentication. It can also normalize request data, set language or region values, prepare shared objects for templates, or modify the response before it reaches the browser. That makes it a small but powerful control point for Astro sites that mix content pages, app-like routes, and API endpoints.
A useful mental model is this: components render content, routes define where content lives, and middleware defines what should happen to the request before rendering starts. When the same decision needs to be made everywhere, middleware is usually the cleanest place to put it.
In Astro, the middleware file is typically src/middleware.ts or src/middleware.js, and it exports an onRequest() handler. That handler receives a context object and a next() function. The context gives you access to request information and a locals object; next() continues the render pipeline. If you have used server hooks in other frameworks, this will feel familiar, but Astro’s version is intentionally lightweight and focused on request-scoped work.
Why it matters
Middleware matters because it reduces duplication and keeps request logic in one place. Without it, teams often spread the same checks across layouts, endpoints, and page components, which makes maintenance harder and increases the chance of inconsistent behavior.
From a business perspective, that consistency affects how safely you can personalize content, gate access, or adjust page behavior by request. If a merchant site needs a logged-in customer to see account-specific navigation, or a region-aware storefront to set the right defaults, middleware helps enforce that logic before the page is assembled.
From a technical perspective, middleware is also the right layer for request-specific data that should not be globally cached. A user session, a feature flag, or a locale value belongs to the current request, not to a shared app-wide store. context.locals gives you a structured way to pass that data forward without stuffing it into query parameters or duplicating fetches.
It also improves clarity. When a developer reads middleware, they can usually tell which cross-cutting concerns exist in the app: auth, redirects, request enrichment, rewrites, or response transformations. That makes the site easier to reason about, especially as it grows beyond a simple brochure site.
For teams shipping commerce or content-heavy experiences, the payoff is often operational as much as architectural. A single middleware layer can keep preview traffic separate from public traffic, apply a consistent locale fallback, or ensure that every protected route checks the same session rules. That reduces edge-case bugs that are hard to spot in isolated components.
There is also a performance angle. Middleware can prevent unnecessary work by stopping a request early, skipping expensive data loading for public routes, or normalizing values once instead of repeating parsing logic in several places. Used well, it makes the request path simpler, not heavier.
How it works
Astro middleware works by exporting an onRequest() handler from src/middleware.ts or src/middleware.js. That handler receives a context object and a next() function. The context object carries request information and a locals object, while next() continues the rendering process.
The flow is straightforward. First, the request enters the middleware. Then your code can inspect headers, cookies, route data, or other context values. After that, you can attach data to context.locals, return a response immediately, or call next() to continue to the page or endpoint.
If you call next(), Astro continues rendering the route. Any values you placed in context.locals remain available to later parts of the request, including .astro files and API routes. That is why locals are so useful for request-scoped data: they are created for the current render and discarded when that request is done.
Middleware can also change the outcome of the request. For example, it can rewrite a request to another route without changing the URL in the browser, or it can return a custom response before the page renders. That makes it possible to handle access control, request normalization, or HTML transformations at a single entry point.
The request lifecycle in plain language
Think of the lifecycle as three steps. The request arrives, middleware runs, and the route renders or exits early. If you need to share data, place it in context.locals. If you need to stop the request, return a response. If you need the page to continue, call next().
That simple pattern is easy to extend. A localization layer might inspect the Accept-Language header, set context.locals.locale, and then let the page render with the correct copy. An auth layer might check cookies and either set context.locals.user or rewrite the visitor to a login route.
A practical detail worth noting is that middleware can participate in both build-time and request-time rendering. That means the same pattern can support prerendered content and on-demand pages, but the available request features differ. If your logic depends on cookies, headers, or other live request data, make sure the route is rendered on demand. If it only needs to enrich shared data for a prerendered page, middleware can still be useful during the build.
Another useful way to think about the mechanism is as a small pipeline. Middleware can read, enrich, decide, and pass along. Read the incoming request once. Enrich it with normalized values. Decide whether the request should continue. Pass the result to the next layer. That pipeline keeps request handling predictable and makes it easier to test each step separately.
Use cases
One common use case is request-aware personalization. A merchant may want the header, pricing notes, or account links to change depending on whether a visitor is signed in. Middleware can load the session once and make a user object available to the rest of the request so the page can render the right state without extra checks in every component.
Another use case is localization and regional routing. If a site serves multiple markets, middleware can detect locale from the request and store it in locals for downstream components. That is especially helpful when the same page template needs to render different labels, currency hints, or region-specific navigation based on the current request.
A third use case is response control for app-like flows. Middleware can decide whether a request should continue, be rewritten, or be blocked. That is useful for protected pages, preview flows, maintenance screens, or any route where the outcome depends on request data rather than static content alone.
In practice, these use cases often overlap. A storefront might use middleware to read the session, set locale, and decide whether a customer should see a rewritten account page or a public fallback. The value is not only in the individual checks, but in keeping them in one predictable layer.
It is also useful for cross-cutting observability and request shaping. For example, a team can attach a request ID, normalize a tenant or market code, or prepare a lightweight analytics flag in locals so downstream code does not need to repeat the same parsing logic. That keeps page components focused on rendering instead of request interpretation.
A good rule of thumb is to use middleware when the same request decision affects multiple routes or multiple layers of the same route. Avoid using it for one-off content tweaks that only matter inside a single component. If the logic is not shared, the middleware layer is usually more ceremony than value.
How to implement or apply it
Start by creating src/middleware.ts or src/middleware.js and exporting onRequest(). Keep the handler small at first. A good first implementation usually does one thing: read a request signal, store a value in context.locals, and continue with next().
A practical pattern is to define the data you want to share before you render the page. For example, if you need a user, locale, or featureFlag, assign it to context.locals inside middleware and read it later through Astro.locals in your .astro files. That keeps the request contract explicit.
If you are using TypeScript, extend the global App.Locals namespace in env.d.ts. This gives you autocomplete and helps prevent mismatches between middleware and page code. It is especially valuable when multiple developers touch the same request data, because the types document what is available and what shape it has.
When the logic gets more complex, consider whether it should be split into multiple middleware functions and chained with sequence(). That is useful when one concern validates the request, another enriches it, and a third decides on the response. Chaining keeps each step focused instead of turning one file into a long conditional block.
A practical implementation workflow
- Identify the request data you need downstream.
- Decide whether the data belongs in locals, a rewrite, or an early response.
- Add the middleware handler and keep the first version narrow.
- Type
App.Localsif the data is shared across multiple files. - Test both prerendered and on-demand routes if your app uses both.
A good rule is to use middleware for request-scoped decisions, not for long-term storage or heavy business logic. If the data must persist across multiple visits, it belongs in a database, session store, or another durable system. Middleware should prepare the request, not become the application state layer.
When applying middleware to a real workflow, start with the smallest useful slice. For example, if you only need a locale value to render a header and a product note, do not also add cart logic, pricing rules, and analytics in the same handler. Separate concerns make it easier to test the request path and easier to remove or replace one piece later.
A useful implementation check is to ask what downstream code actually needs. If a page only needs a boolean like isLoggedIn, do not pass the entire session object unless another route truly requires it. Smaller locals are easier to type, easier to inspect, and less likely to leak unnecessary data into templates.
You can also use middleware as a normalization layer. For example, if different parts of the app refer to the same market by different codes or casing, middleware can convert that input into one canonical value before anything else reads it. That kind of cleanup is subtle, but it prevents a lot of conditional branching later.
When deciding between middleware and page logic, ask two questions: does the value need to be available to more than one downstream file, and does it depend on the current request? If both answers are yes, middleware is a strong fit. If the answer to either question is no, keep the logic closer to the component or route that actually needs it.
Common mistakes and pitfalls
The most common mistake is overusing middleware for logic that belongs elsewhere. If a value is static and never changes per request, it does not need to be loaded in middleware. Putting too much there can make the request path harder to debug and slower to reason about.
Another pitfall is assuming locals is persistent. It is not. context.locals exists only for the current request, so it is the wrong place for data that must survive across multiple visits or sessions. If the app needs durable state, store it somewhere designed for that purpose.
Teams also run into trouble when they forget to type shared locals. Without a declared App.Locals interface, it is easy for one file to assume a property exists while another file sets a different shape or name. Type safety matters here because middleware is often the hidden dependency behind several pages.
A final issue is mixing up rewrites, redirects, and response replacement. A rewrite changes what content is served without changing the browser URL, while a redirect sends the visitor elsewhere. If you need a different URL, use a redirect; if you need different content behind the same URL, middleware plus rewrite is the better fit.
There is also a performance pitfall: doing expensive work for every request when only a small subset of routes needs it. If authentication only matters in the account area, scope the middleware or branch early so public pages do not pay for unnecessary checks. Likewise, avoid mutating locals with large objects when a small normalized value would do.
Another subtle mistake is letting middleware become a second application layer. If the handler starts fetching multiple services, transforming large payloads, and making several branching decisions, it can become harder to test than the pages it supports. In that case, move reusable business logic into a separate server utility and keep middleware as the orchestration layer.
One more practical trap is forgetting to account for build-time rendering. If a route is prerendered, middleware may run without the same live request features you would expect from an on-demand page. That means code that depends on cookies or headers should be written carefully and tested in the rendering mode the route actually uses.
Best practices and quick checklist
Keep middleware focused on request concerns that need to be shared or enforced early. If the logic does not depend on the request, it probably does not belong there. That discipline keeps middleware small and easier to test.
Use context.locals as the handoff point between request logic and rendering. Name values clearly, keep their shape stable, and document them with TypeScript when possible. The more predictable locals are, the less defensive code you need in pages and endpoints.
Prefer chaining when you have distinct concerns. Authentication, localization, and response shaping can each live in their own middleware function, which makes the flow easier to read than one large conditional block. That separation also helps when one step needs to be reused or reordered later.
A practical checklist can keep the implementation honest: confirm the route really needs request-time logic, confirm the data is request-scoped, confirm the downstream files know the locals shape, and confirm the chosen action is correct for the user experience. If any of those answers are unclear, the middleware likely needs to be simplified.
Quick checklist:
- Store only request-scoped data in locals.
- Type shared locals in
env.d.ts. - Use
next()when the request should continue. - Use a rewrite when the content should change without changing the URL.
- Test both prerendered and on-demand routes.
- Keep middleware small enough to understand at a glance.
- Branch early so public routes skip expensive work.
- Prefer simple values in locals unless downstream code truly needs a richer object.
- Separate request orchestration from reusable business logic.
- Verify that any cookie- or header-based code matches the route’s rendering mode.
If you are deciding whether to add middleware at all, ask one question: does this logic need to run before every matching request and be visible to multiple downstream files? If the answer is yes, middleware is likely the right layer.
From practice — illustrative scenario (hypothetical, not a client project)
Illustrative example — not a real client project: Imagine a merchant running a content-heavy Astro site with a small customer area. The public pages are prerendered, but the account section is rendered on demand because it depends on session data. The team wants the header to show a signed-in state, the account page to know the visitor’s locale, and certain routes to be blocked unless a session exists.
A typical setup would start with middleware that reads cookies, checks whether a session is present, and stores a minimal user object plus locale in context.locals. Public pages can ignore that data, but account pages and shared layout components can read it through Astro.locals and render the correct navigation or copy.
The first problem the team might notice is duplication. Without middleware, each page would need its own session lookup and locale parsing. That creates repeated code and increases the chance that one page behaves differently from another. Middleware solves that by making the request data available once, in one place.
The next decision is what to do when the session is missing. If the route is protected, middleware can rewrite the request to a login page or return a response early. If the route is public but personalized, it can simply continue and let the page render with a fallback state. That distinction matters because not every request needs the same level of enforcement.
A sensible workflow for the team would be to map routes into three groups: public, personalized, and protected. Public routes skip most middleware work. Personalized routes enrich locals but continue. Protected routes validate access first and only then render. That structure keeps the logic understandable and prevents every page from carrying the same cost.
The team would also want to define the locals contract up front. For example, user might be nullable, locale might always be a short string, and isPreview might be a boolean. Once those shapes are typed, page authors can rely on them without guessing whether a property exists.
If the team later adds a second concern, such as a feature flag for a new account dashboard, they can chain another middleware step instead of expanding the first one indefinitely. One step can validate the session, another can set the flag, and a third can decide whether to rewrite to a beta route. That keeps each concern visible and easier to remove if the rollout changes.
The takeaway is not that middleware replaces all page logic. It is that middleware gives the app a clean boundary for request-aware decisions. Once the team understands that boundary, the rest of the code becomes easier to structure: pages render content, locals carry shared request data, and middleware handles the decisions that must happen first.
Related concepts and further reading
Middleware is easiest to use when you already understand how Astro shares data and renders routes. These related guides cover the pieces that usually sit next to it in a real project.
- Astro content collections guide — useful when middleware feeds request-aware data into structured content.
- Astro islands architecture — helpful for deciding what should stay static and what should react to requests.
- Astro Themes — browse Astro-ready site foundations when you want to apply middleware patterns in a real project.
- Astro view transitions guide — relevant when request handling and navigation behavior need to work together.
- Astro docs on middleware — the official reference for context, locals, chaining, and rewrites.
Explore this topic
More Astro guides, glossary entries, and practical workflows live on the topic hub.
Frequently asked questions
What does Astro middleware do?
Astro middleware intercepts a request before a page or endpoint finishes rendering. That gives you a place to read request data, set shared values in locals, rewrite the route, or return a response early. It is useful when the same logic needs to run across multiple pages instead of being repeated inside each component.
When should I use locals in Astro middleware?
Use locals when request-specific data needs to be available later in the same render cycle. Common examples include a user object, a feature flag, or a locale value that pages and API routes both need. Locals are temporary and live only for that request, so they are not a storage layer for persistent data.
Can Astro middleware run on prerendered pages?
Yes, middleware can still run during build time for prerendered pages, but the request context is different from on-demand rendering. For routes rendered on demand, middleware runs when the route is requested, which makes request features like cookies and headers available. The exact behavior depends on how the route is rendered.
How do I type Astro.locals?
Astro recommends extending the global App.Locals namespace in your env.d.ts file. That gives you autocomplete and type safety in both middleware and .astro files. It is the cleanest way to keep shared request data consistent across the app.
What is the difference between middleware and rewrites in Astro?
Middleware is the place where you decide what should happen to a request, while a rewrite is one possible action you can take from that layer. A rewrite shows different content without sending the visitor to a new URL. Use middleware for the logic and rewrites for the routing outcome.
Does Astro middleware replace page-level logic?
No. Middleware is best for request-scoped concerns that need to be shared or enforced before rendering, such as auth checks, locale detection, or request normalization. Page-level logic is still appropriate for content-specific decisions, component state, and rendering details that do not need to be reused across routes.