diff --git a/docs/02-building-your-application/01-routing/01-pages-and-layouts.md b/docs/02-building-your-application/01-routing/01-pages-and-layouts.md
index dc7ad660e..fdb928370 100644
--- a/docs/02-building-your-application/01-routing/01-pages-and-layouts.md
+++ b/docs/02-building-your-application/01-routing/01-pages-and-layouts.md
@@ -107,3 +107,36 @@ export default async function Layout({ children }: { children: JSX.Element }, {
```
The `fetch` is directly native and has no wrapper to control the cache. We recommend that you do not do the same `fetch` in several places, but use the [`context`](/docs/building-your-application/data-fetching/request-context) to store the data and consume it from any component.
+
+## Response headers in layouts and pages
+
+The `responseHeaders` function can be exported inside the `layout` and inside any `page`. In the same way that is possible to export it also in the [`middleware`](docs/building-your-application/routing/middleware).
+
+All `responseHeaders` will be mixed in this order:
+
+1. `middleware` response headers
+2. `layout` response headers (can crush the middleware response headers)
+3. `page` response headers (both middleware and layout response headers can be mixed).
+
+```ts filename="middleware.ts" switcher
+import { type RequestContext } from "brisa";
+
+export function responseHeaders(
+ request: RequestContext,
+ responseStatus: number,
+) {
+ return {
+ "Cache-Control": "public, max-age=3600",
+ "X-Example": "This header is added from layout",
+ };
+}
+```
+
+```js filename="middleware.js" switcher
+export function responseHeaders(request, responseStatus) {
+ return {
+ "Cache-Control": "public, max-age=3600",
+ "X-Example": "This header is added from layout",
+ };
+}
+```
diff --git a/docs/02-building-your-application/01-routing/05-api-routes.md b/docs/02-building-your-application/01-routing/05-api-routes.md
index e8cc88751..50e2a9666 100644
--- a/docs/02-building-your-application/01-routing/05-api-routes.md
+++ b/docs/02-building-your-application/01-routing/05-api-routes.md
@@ -54,15 +54,15 @@ You can read the `Request` body using the standard Web API methods:
```ts filename="src/api/items/route.ts" switcher
export async function POST(request: RequestContext) {
- const res = await request.json()
- return new Response(JSON.stringify({ res }))
+ const res = await request.json();
+ return new Response(JSON.stringify({ res }));
}
```
```js filename="src/api/items/route.js" switcher
export async function POST(request) {
- const res = await request.json()
- return new Response(JSON.stringify({ res }))
+ const res = await request.json();
+ return new Response(JSON.stringify({ res }));
}
```
@@ -72,19 +72,19 @@ You can read the `FormData` using the standard Web API methods:
```ts filename="src/api/items/route.ts" switcher
export async function POST(request: RequestContext) {
- const formData = await request.formData()
- const name = formData.get('name')
- const email = formData.get('email')
- return new Response(JSON.stringify({ name, email }))
+ const formData = await request.formData();
+ const name = formData.get("name");
+ const email = formData.get("email");
+ return new Response(JSON.stringify({ name, email }));
}
```
```js filename="src/api/items/route.js" switcher
export async function POST(request) {
- const formData = await request.formData()
- const name = formData.get('name')
- const email = formData.get('email')
- return new Response(JSON.stringify({ name, email }))
+ const formData = await request.formData();
+ const name = formData.get("name");
+ const email = formData.get("email");
+ return new Response(JSON.stringify({ name, email }));
}
```
@@ -198,18 +198,18 @@ You can add the `Cache-Control` headers to the response. By default is not using
```ts filename="app/items/route.ts" switcher
export async function GET() {
- const data = await getSomeData()
- const res = new Response(JSON.stringify(data))
+ const data = await getSomeData();
+ const res = new Response(JSON.stringify(data));
- res.headers.set("Cache-Control", "max-age=86400")
+ res.headers.set("Cache-Control", "max-age=86400");
- return res
+ return res;
}
```
## Headers and Cookies
-You can read headers and cookies from the [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and write headers and cookies to the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) using Web APIs.
+You can read headers and cookies from the [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and write headers and cookies to the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) using Web APIs.
Example reading/writing cookies:
@@ -217,20 +217,20 @@ Example reading/writing cookies:
import { type RequestContext } from "brisa";
export async function GET(request: RequestContext) {
- const cookies = request.headers.get('cookie')
- const res = new Response('Hello, Brisa!');
+ const cookies = request.headers.get("cookie");
+ const res = new Response("Hello, Brisa!");
if (cookies) {
- res.headers.set('set-cookie', cookies)
+ res.headers.set("set-cookie", cookies);
}
- return res
+ return res;
}
```
## Streaming
- You can use the Web APIs to create a [stream](https://bun.sh/docs/api/streams) and then return it inside the `Response`:
+You can use the Web APIs to create a [stream](https://bun.sh/docs/api/streams) and then return it inside the `Response`:
```ts
export async function GET() {
@@ -243,7 +243,7 @@ export async function GET() {
},
});
- return new Response(stream) // Hello Brisa!
+ return new Response(stream); // Hello Brisa!
}
```
diff --git a/docs/02-building-your-application/01-routing/06-middleware.md b/docs/02-building-your-application/01-routing/06-middleware.md
new file mode 100644
index 000000000..f4684614a
--- /dev/null
+++ b/docs/02-building-your-application/01-routing/06-middleware.md
@@ -0,0 +1,159 @@
+---
+title: Middleware
+description: Learn how to use Middleware to run code before a request is completed.
+---
+
+Middleware allows you to run code before a request is completed. Then, based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly.
+
+Middleware runs before routes are matched. See [Matching Paths](#matching-paths) for more details.
+
+## Convention
+
+Use the file `middleware.ts` (or `.js`) inside the `src` folder of your project to define Middleware. Or inside `src/middleware/index.ts` (or `.js`).
+
+## Example
+
+```ts filename="middleware.ts" switcher
+import { type RequestContext } from "brisa";
+
+// This function can be without `async` if you are not using `await` inside
+export default async function middleware({
+ i18n,
+ route,
+ headers,
+}: RequestContext): Response | undefined {
+ const { locale } = i18n;
+ const isUserRoute = route?.name?.startsWith("/user/[username]");
+
+ if (isUserRout && !(await isUserLogged(headers))) {
+ return new Response("", {
+ status: 302,
+ headers: {
+ Location: `/${locale}/login`,
+ },
+ });
+ }
+}
+```
+
+```js filename="middleware.js" switcher
+// This function can be without `async` if you are not using `await` inside
+export default async function middleware({ i18n, route, headers }) {
+ const { locale } = i18n;
+ const isUserRoute = route?.name?.startsWith("/user/[username]");
+
+ if (isUserRout && !(await isUserLogged(headers))) {
+ return new Response("", {
+ status: 302,
+ headers: {
+ Location: `/${locale}/login`,
+ },
+ });
+ }
+}
+```
+
+Only is possible to access to `route` property inside `api routes` and `pages routes`. This is to support handling of dynamic routes, catch-all, etc in a simple way. In the case of `assets` you can look it up through the request:
+
+```ts filename="middleware.ts" switcher
+import { type RequestContext } from "brisa";
+
+export default async function middleware(
+ request: RequestContext,
+): Response | undefined {
+ const url = new URL(request.url);
+
+ if (url.pathname === "/favicon.svg") {
+ return new Response(
+ `
+
+ `,
+ {
+ headers: { "content-type": "image/svg+xml" },
+ },
+ );
+ }
+}
+```
+
+```js filename="middleware.js" switcher
+export default async function middleware(request) {
+ const url = new URL(request.url);
+
+ if (url.pathname === "/favicon.svg") {
+ return new Response(
+ `
+
+ `,
+ {
+ headers: { "content-type": "image/svg+xml" },
+ },
+ );
+ }
+}
+```
+
+However, this is not the best way to serve assets. You can put the static files directly inside the `public` folder. More information [here](/building-your-application/optimizing/static-assets).
+
+## Cookies & Headers
+
+### On Request
+
+Cookies are regular headers. On a `Request`, they are stored in the `Cookie` header.
+
+```ts filename="middleware.ts" switcher
+import { type RequestContext } from "brisa";
+
+export default async function middleware(request: RequestContext) {
+ const cookies = request.headers.get("cookie");
+ const headers = request.headers.get("x-example");
+
+ // ... do something with cookies and headers
+}
+```
+
+```js filename="middleware.js" switcher
+export default async function middleware(request) {
+ const cookies = request.headers.get("cookie");
+ const headers = request.headers.get("x-example");
+
+ // ... do something with cookies and headers
+}
+```
+
+### On Response
+
+The `responseHeaders` function can be exported in the `middleware`, in the same way that you can do it inside `layout` and `pages`.
+
+All responseHeaders will be mixed in this order:
+
+1. `middleware` response headers
+2. `layout` response headers (can crush the middleware response headers)
+3. `page` response headers (both middleware and layout response headers can be mixed).
+
+```ts filename="middleware.ts" switcher
+import { type RequestContext } from "brisa";
+
+export function responseHeaders(
+ request: RequestContext,
+ responseStatus: number,
+) {
+ return {
+ "Cache-Control": "public, max-age=3600",
+ "X-Example": "This header is added from middleware",
+ };
+}
+```
+
+```js filename="middleware.js" switcher
+export function responseHeaders(request, responseStatus) {
+ return {
+ "Cache-Control": "public, max-age=3600",
+ "X-Example": "This header is added from middleware",
+ };
+}
+```
diff --git a/src/cli/serve.tsx b/src/cli/serve.tsx
index c750741dc..a73c04390 100644
--- a/src/cli/serve.tsx
+++ b/src/cli/serve.tsx
@@ -45,7 +45,8 @@ if (!fs.existsSync(PAGES_DIR)) {
process.exit(1);
}
-const customMiddleware = await importFileIfExists("middleware", ROOT_DIR);
+const middlewareModule = await importFileIfExists("middleware", ROOT_DIR);
+const customMiddleware = middlewareModule?.default;
let pagesRouter = getRouteMatcher(PAGES_DIR, RESERVED_PAGES);
let rootRouter = getRouteMatcher(ROOT_DIR);
@@ -122,7 +123,6 @@ console.log(
///////////////////////////////////////////////////////
////////////////////// HELPERS ///////////////////////
///////////////////////////////////////////////////////
-
async function handleRequest(req: RequestContext, isAnAsset: boolean) {
const locale = req.i18n.locale;
const url = new URL(req.finalURL);
@@ -131,11 +131,14 @@ async function handleRequest(req: RequestContext, isAnAsset: boolean) {
const isApi = pathname.startsWith(locale ? `/${locale}/api/` : "/api/");
const api = isApi ? rootRouter.match(req) : null;
+ req.route = isApi ? api?.route : route;
+
// Middleware
if (customMiddleware) {
const middlewareResponse = await Promise.resolve().then(() =>
customMiddleware(req),
);
+
if (middlewareResponse) return middlewareResponse;
}
@@ -148,9 +151,6 @@ async function handleRequest(req: RequestContext, isAnAsset: boolean) {
if (isApi && api?.route && !api?.isReservedPathname) {
const module = await import(api.route.filePath);
const method = req.method.toUpperCase();
-
- req.route = api.route;
-
const response = module[method]?.(req);
if (response) return response;
@@ -192,20 +192,21 @@ async function responseRenderedPage({
const layoutPath = getImportableFilepath("layout", ROOT_DIR);
const layoutModule = layoutPath ? await import(layoutPath) : undefined;
- req.route = route;
-
const pageElement = (
);
+ const middlewareResponseHeaders =
+ middlewareModule?.responseHeaders?.(req, status) ?? {};
const layoutResponseHeaders =
layoutModule?.responseHeaders?.(req, status) ?? {};
const pageResponseHeaders = module.responseHeaders?.(req, status) ?? {};
const htmlStream = renderToReadableStream(pageElement, req, module.Head);
const responseOptions = {
headers: {
+ ...middlewareResponseHeaders,
...layoutResponseHeaders,
...pageResponseHeaders,
"transfer-encoding": "chunked",
diff --git a/src/utils/import-file-if-exists/index.test.ts b/src/utils/import-file-if-exists/index.test.ts
index 40346e897..554b2b0cc 100644
--- a/src/utils/import-file-if-exists/index.test.ts
+++ b/src/utils/import-file-if-exists/index.test.ts
@@ -11,8 +11,8 @@ describe("utils", () => {
describe("importFileIfExists", () => {
it("should return null if there is not a custom middleware", async () => {
- const middleware = await importFileIfExists("middleware");
- expect(middleware).toBeNull();
+ const middlewareModule = await importFileIfExists("middleware");
+ expect(middlewareModule).toBeNull();
});
it('should return custom middleware if "middleware.ts" exists', async () => {
@@ -20,7 +20,7 @@ describe("utils", () => {
join(import.meta.dir, "..", "..", "__fixtures__", "middleware");
const middleware = await importFileIfExists("middleware");
- expect(middleware).toBeFunction();
+ expect(middleware?.default).toBeFunction();
});
it("should return null if there is not a custom i18n", async () => {
@@ -33,7 +33,7 @@ describe("utils", () => {
join(import.meta.dir, "..", "..", "__fixtures__", "i18n");
const i18n = await importFileIfExists("i18n");
- expect(i18n).toEqual({
+ expect(i18n?.default).toEqual({
defaultLocale: "en",
locales: ["en", "fr"],
messages: {
diff --git a/src/utils/import-file-if-exists/index.ts b/src/utils/import-file-if-exists/index.ts
index 0a931ce24..c78166daf 100644
--- a/src/utils/import-file-if-exists/index.ts
+++ b/src/utils/import-file-if-exists/index.ts
@@ -11,7 +11,5 @@ export default async function importFileIfExists(
if (!path) return null;
- const middlewareModule = await import(path);
-
- return middlewareModule.default;
+ return await import(path);
}