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); }