15

Is there a recommended pattern in Remix for running common code on every request, and potentially adding context data to the request? Like a middleware? A usecase for this might be to do logging or auth, for example.

The one thing I've seen that seems similar to this is loader context via the getLoadContext API. This lets you populate a context object which is passed as an arg to all route loaders.

It does work, and initially seems like the way to do this, but the docs for it say...

It's a way to bridge the gap between the adapter's request/response API with your Remix app

This API is an escape hatch, it’s uncommon to need it

...which makes me think otherwise, because

  • This API is explicitly for custom integrations with the server runtime. But it doesn't seem like middlewares should be specific to the server runtime - they should just be part of the 'application' level as a Remix feature.

  • Running middlewares is a pretty common pattern in web frameworks!

So, does Remix have any better pattern for middleware that runs before every loader?

davnicwil
  • 28,487
  • 16
  • 107
  • 123

3 Answers3

15

Instead of middleware, you can call a function directly inside the loader, this will also be more explicit. If you want to early return a response from those "middlewares" Remix let you throw the response object.

For example, if you wanted to check the user has a certain role you could create this function:

async function verifyUserRole(request: Request, expectedRole: string) {
  let user = await getAuthenticatedUser(request); // somehow get the user
  if (user.role === expectedRole) return user;
  throw json({ message: "Forbidden" }, { status: 403 });
}

And in any loader call it this way:

let loader: LoaderFunction = async ({ request }) => {
  let user = await verifyUserRole(request, "admin");
  // code here will only run if user is an admin
  // and you'll also get the user object at the same time
};

Another example could be to require HTTPS

function requireHTTPS(request: Request) {
  let url = new URL(request.url);
  if (url.protocol === "https:") return;
  url.protocol = "https:";
  throw redirect(url.toString());
}

let loader: LoaderFunction = async ({ request }) => {
  await requireHTTPS(request);
  // run your loader (or action) code here
};
  • 2
    Thanks Sergio. Yeah interesting, when I think about it, this pattern is actually really good for just being simple and explicit. No esoteric middleware signatures for different types of behaviours and types just work in the most straightforward manner possible - no need for globals / generics or any such stuff. – davnicwil Dec 05 '21 at 11:22
  • Would you do this check in every loader and action? Even in the nested ones? Or just one check in the parent (“top”) loader? I asked a similar thing in the discord: https://discord.com/channels/770287896669978684/1036360573207711784 – Dimitar Dimitrov Oct 31 '22 at 07:02
  • For things like requireHTTPS I would do it only on the root route, but for authentication I would do it on every loader. – Sergio Xalambrí Nov 02 '22 at 19:24
  • 2
    With all due respect, but isn't the whole point of middleware (and basically any decorator pattern) that you don't need to modify the handler for it to work? If you can modify the handler then sure you don't need middleware... But this answer just sidesteps the issue i.s.o answering it. I prefer [the answer](/a/70187165/286685) by [Kaidjin](/users/2236690/kaidjin) which basically just tells you, no, middleware is not (yet?) supported by Remix. – Stijn de Witt Nov 19 '22 at 16:35
  • I totally agree with Stijn. The whole idea of manually checking authentication on every loader and action feels bad. Leaves an unnecessary place for human error. I come from React/Django world where I can write the authentication check only once for both backend and frontend and then when I add a new feature, it's already taken care of. I hope they come up with some sort of general solution. – Janne Jan 30 '23 at 11:46
5

There is no way inside Remix to run code before loaders.

As you found out, there is the loader context but it runs even before remix starts to do its job (so you won't know which route modules are matched for example).

You can also run arbitrary code before handing the request to remix in the JS file where you use the adapter for the platform you're deploying to (this depend on the starter you used. This file doesn't exist if you've chosen remix server as your server)

For now it should work for some use cases, but I agree this is a missing feature in remix for now.

Kaidjin
  • 1,433
  • 1
  • 12
  • 18
  • yep interesting - thanks for the anwer - having pondered on this / played around with Remix a bit more I'm now realising that this missing feature of global middleware might actually be, erm, a feature (as in, deliberately omitted by design in Remix to make things simpler / more straightforward) – davnicwil Dec 05 '21 at 11:25
3

Inside app/root.tsx

export let loader: LoaderFunction = ({ request }) => {

const url = new URL(request.url);
const hostname = url.hostname;
const proto = request.headers.get("X-Forwarded-Proto") ?? url.protocol;

url.host =
  request.headers.get("X-Forwarded-Host") ??
  request.headers.get("host") ??
  url.host;
  url.protocol = "https:";

if (proto === "http" && hostname !== "localhost") {
  return redirect(url.toString(), {
    headers: {
      "X-Forwarded-Proto": "https",
    },
  });
}
  return {};
};

Source: https://github.com/remix-run/remix-jokes/blob/8f786d9d7fa7ea62203e87c1e0bdaa9bda3b28af/app/root.tsx#L25-L46

Ogie
  • 1,304
  • 2
  • 14
  • 17
  • I'm curious if we could do something like this for only some routes. I imagine we can have a list of protected routes and want a header for those routes but not others. Will have to experiment a bit and see how that works – David Yarzebinski Oct 05 '22 at 00:06