# Middleware and Cookies

Course: [Astro](https://www.devtutorials.ai/learn/astro) · Section: Server Development
Source: https://www.devtutorials.ai/learn/astro/server/middleware-and-cookies

> What middleware is, src/middleware.ts, defineMiddleware(), next() and changing responses, sequence(), redirects and rewrites in middleware, sharing data with locals, App.Locals, and reading, setting and deleting cookies with their options.

## Middleware

- **Middleware** = code that runs **before** a page or endpoint, for every request.
- It can read the request, add data for the page, or answer early with a redirect.
- It can also run **after** the page, to change the response.
- In this lesson, you will write middleware and use **cookies**, small pieces of data stored in the visitor's browser.

**Simple lesson:** middleware is a gate every request passes through.

### A Real-World Example

- Asha wants every page to know the visitor's preferred language and to log how long each page takes.
- Adding that code to every page would mean copying it many times.
- She writes it once in `src/middleware.ts`. Every request runs it.

Press **▶ Run**. Visit both pages. The **Terminal** shows the middleware running before each one.

<a href="/about">About</a>
`,
    "src/pages/about.astro": `<h1>About</h1>
<p>The middleware said: {Astro.locals.greeting}</p>
<a href="/">Home</a>
`,
  }}
/>

What happened:

- `onRequest` ran first. It put `greeting` into `context.locals`.
- `await next()` ran the page. The page read `Astro.locals.greeting`.
- After `next()`, the middleware ran again and logged the time.

### What You Will Learn

In this lesson, you will learn:

1. How to create middleware in `src/middleware.ts`.
2. What `next()` does, and how to change the response.
3. How to chain several middleware with `sequence()`.
4. How to redirect or rewrite from middleware.
5. How to share data with pages through `locals`.
6. How to read, set and delete cookies.
7. Which cookie options keep them safe.

## Creating Middleware

### src/middleware.ts or src/middleware.js

- **[TS]** Create `src/middleware.ts`. **[JS]** Create `src/middleware.js`.
- Export a function called `onRequest`.
- It runs for every on-demand request. For static pages, it runs once each, at build time.

### defineMiddleware()

- `defineMiddleware()` from `astro:middleware` wraps your function.
- It changes nothing at run time. It gives your editor the right types for `context` and `next`.

### next() and Modifying Responses

- `next()` = "run the rest: other middleware, then the page".
- It returns the page's `Response`.
- After it, you can change the response, such as adding a header: `response.headers.set("X-Powered-By", "Asha")`.
- If you never call `next()`, the page never runs. You must then return your own response.

<figure className="ast-fig ngc-fig">
<svg viewBox="0 0 640 190" role="img" aria-labelledby="fig-mw" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-mw">Animation: a request enters middleware, which calls next(), the page runs, and the response passes back through the middleware before reaching the browser.</title>
  <defs><marker id="mw-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-slate)" }} /></marker></defs>
  <rect x="20" y="60" width="110" height="70" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} />
  <text x="75" y="100" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Browser</text>
  <rect x="220" y="30" width="180" height="130" rx="12" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} />
  <text x="310" y="54" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Middleware</text>
  <text x="310" y="80" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--mono)", fontSize: 11 }}>before: set locals</text>
  <text x="310" y="104" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--mono)", fontSize: 12, fontWeight: 700 }}>await next()</text>
  <text x="310" y="128" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--mono)", fontSize: 11 }}>after: add headers</text>
  <rect x="490" y="60" width="130" height="70" rx="10" style={{ fill: "var(--ngc-orange)" }} />
  <text x="555" y="100" textAnchor="middle" style={{ fill: "#fff", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Page</text>
  <path d="M130 80 H216" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#mw-arrow)" />
  <path d="M400 80 H486" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#mw-arrow)" />
  <path d="M490 112 H404" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#mw-arrow)" />
  <path d="M220 112 H134" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#mw-arrow)" />
  <circle r="7" style={{ fill: "var(--ngc-green)" }}><animateMotion dur="5s" repeatCount="indefinite" path="M130 80 H490 V112 H130" /></circle>
</svg>
<figcaption>The request goes in through the middleware, and the response comes back out through it.</figcaption>
</figure>

### Chaining with sequence()

- `sequence(a, b, c)` from `astro:middleware` runs several middleware in order.
- Each one calls `next()` to pass control to the next.
- Keep each middleware small, with one job: logging, auth, language and so on.

Order matters in `sequence()`. Put logging first so it sees every request, and auth before anything that needs to know the user.

Press **▶ Run**. Read the order in the **Terminal**: in through `log`, then `lang`, out in reverse.

<a href="/?lang=ta">தமிழ்</a> · <a href="/?lang=en">English</a>
`,
  }}
/>

**Simple lesson:** `onRequest` runs around every request. `next()` runs the page. `sequence()` chains several.

## Redirects, Rewrites and locals

### Redirecting and Rewriting from Middleware

- `return context.redirect("/login")` = send the visitor elsewhere. The page does not run.
- `return context.rewrite("/maintenance")` = show another page's content at the same address.
- Use these for rules that apply to many pages, such as "every `/admin` page needs a login".

### context.locals

- **locals** = an object that lives for one request only.
- Middleware writes to it: `context.locals.user = ...`.
- Pages read it: `Astro.locals.user`. Endpoints read `context.locals.user`.
- Nothing in `locals` reaches the browser unless a page shows it.

### [TS] Typing App.Locals

- Describe your locals in `src/env.d.ts`, so pages know their types:

```ts
declare namespace App {
  interface Locals {
    lang: "en" | "ta";
    user?: { name: string };
  }
}
```

- **[JS]** You do not need this. `locals` is a plain object.

<p>You were redirected here by the middleware.</p>
`,
  }}
/>

Config `redirects` (from **Routing**) are simpler for fixed lists. Middleware redirects are for rules, such as "anything that starts with `/old-blog`".

**Simple lesson:** middleware can redirect or rewrite, and passes data to pages through `locals`.

## Cookies

### What Cookies Are

- **Cookie** = a small piece of text the browser stores for one website, and sends back with every request to it.
- Examples: a theme choice, a "seen the banner" flag, a session ID.
- A cookie is per visitor. Each browser has its own.

### Reading Cookies

- `Astro.cookies.get("theme")` returns the cookie, or `undefined`.
- `Astro.cookies.has("theme")` returns `true` or `false`.
- Read its value in the form you need:
  - `.value` = the text,
  - `.number()` = a number,
  - `.boolean()` = `true` or `false`,
  - `.json()` = an object, if you stored JSON.

### Setting Cookies

- `Astro.cookies.set("theme", "dark", { path: "/" })`.
- Objects are stored as JSON: `Astro.cookies.set("prefs", { fontSize: 18 })`.
- Setting a cookie only works in **on-demand** pages, endpoints, middleware and actions.

### Deleting Cookies

- `Astro.cookies.delete("theme", { path: "/" })`.
- Use the same `path` as when you set it.

### Cookies in Endpoints and Middleware

- In endpoints, actions and middleware, use `context.cookies`. It works the same way.

Press **▶ Run**. Visit the page a few times. Then reset the counter. The **Terminal** shows each cookie change.

<a href="/visits">Visit again</a> · <a href="/visits?reset">Reset</a>
`,
  }}
/>

### Cookie Options

- `path` = which addresses get the cookie. `"/"` means the whole site.
- `maxAge` = seconds until it expires. `expires` = an exact date. Without either, it ends when the browser closes.
- `httpOnly` = browser JavaScript cannot read it. Use it for anything sensitive.
- `secure` = only sent over HTTPS.
- `sameSite` = whether other websites' requests carry it:
  - `"strict"` = never,
  - `"lax"` = only for normal link clicks (a good default),
  - `"none"` = always (needs `secure`).
- `domain` = share it with sub-domains, such as `blog.asha.dev`.
- `partitioned` = keep a third-party cookie separate per site. Rarely needed.

Cookies travel with every request and the visitor can edit them. Never store passwords or trust a cookie's value without checking it. Keep private data on the server, in a session.

### Merging and Reading Set-Cookie Headers (Advanced)

- `cookies.merge(other)` joins cookies from another `AstroCookies` object.
- `cookies.headers()` lists the `Set-Cookie` header values Astro will send. Useful in tests and custom middleware.

Press **▶ Run**. Pick a theme. It is saved as a JSON cookie, and read back on every page.

<form method="post">
    <select name="theme">
      <option selected={saved.theme === "light"}>light</option>
      <option selected={saved.theme === "dark"}>dark</option>
    </select>
    <input name="size" type="number" value={saved.size} />
    <button>Save</button>
</form>
`,
  }}
/>

**Simple lesson:** `get()`, `has()`, `set()` and `delete()` cookies. Use `httpOnly`, `secure` and `sameSite` to keep them safe.

## TL;DR

1. **Middleware** = `onRequest(context, next)` in `src/middleware.ts`. Runs around every request.
2. **next()** = run the page. Change the response after it.
3. **sequence(a, b)** = run several middleware in order.
4. **context.redirect() / context.rewrite()** = answer early.
5. **locals** = per-request data. Middleware writes, pages read `Astro.locals`.
6. **Cookie** = small data in the browser, sent with every request.
7. **Read** = `cookies.get(name)?.value / .number() / .boolean() / .json()`, `cookies.has(name)`.
8. **Write** = `cookies.set(name, value, options)`, `cookies.delete(name, { path })`.
9. **Options** = `path`, `maxAge`, `httpOnly`, `secure`, `sameSite`, `domain`.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Not returning `next()` | Calling it is enough | `return next()` or return its response | The page gets no response |
| Middleware in the wrong place | Any file name works | `src/middleware.ts` exporting `onRequest` | It never runs |
| Setting a cookie on a static page | Static pages have requests | Set cookies on on-demand routes | Error: cookies cannot be set |
| Deleting with a different `path` | `delete()` finds every copy | Use the same `path` as `set()` | The cookie stays |
| Secrets in a cookie | Cookies are private | Store an ID, keep data on the server | Visitors can read or change it |
| No `httpOnly` on a session cookie | Scripts need it | Use `httpOnly: true` | Injected scripts can steal it |

</div>

## Check Yourself

## Exercises

## What to Remember

- Middleware in `src/middleware.ts` runs around every request. `next()` runs the page.
- `sequence()` chains small middleware. `locals` passes data to pages.
- Middleware can redirect or rewrite before the page runs.
- Cookies live in the browser. Read with `get()`, write with `set()`, remove with `delete()`.
- Use `httpOnly`, `secure` and `sameSite`, and never trust a cookie blindly.

Next: **Sessions**, where data stays on the server and only an ID lives in a cookie.
