#Return `new Response` from `action` View Model (helper functions)

1 messages · Page 1 of 1 (latest)

cerulean gulch
#

First, a code example:

import { ViewModel } from './api';

export const action = async ({ request, context }) => {
  const response = await ViewModel({ request, context });

  return json(response);
};

Inside of the ViewModel we do things: you know, extract data, validate data, etc. etc. Our validation code is wrapped in a try/catch where we throw an Error if some data doesn't validate. Inside of our catch block we return a new Response() with all of the appropriate settings (content type header json, status: 422 etc). However, it doesn't propagate to the action. The action is sending back a 200. When we copy/paste the new Response() directly into the action everything works as expected.

Context:

  • Node
  • Express
  • Remix ^1.6.3

Questions:

  • Is this a JS issue? Am I misunderstanding scope in Node.js or some other concept (async stuff)? I would think that the new Response returned from our View Model would be as if it was thrown within the action
  • Should we instead surface errors from our View Model to our action/loaders? In other words, we could return from our View Model an object description of the result and then from the action/loader throw a new response.

Thanks in advance

inland fiber
#

return json(response) is basically just a wrapper for

return new Response(JSON.stringify(response), {
  headers: {
    "Content-Type": "application/json; charset=utf-8",
  },
}); 

See the implementation
https://github.com/remix-run/remix/blob/main/packages/remix-server-runtime/responses.ts#L18

Now if you do

const response = new Response("foo", { status: 422 });
const body = JSON.stringify(response); // this is === '{}'
return new Response(body, {
  headers: {
    "Content-Type": "application/json; charset=utf-8",
  },
}); 

this is an entirely new, empty response with the default status code of 200. Since you can put anything into json (see https://github.com/remix-run/remix/blob/main/packages/remix-server-runtime/responses.ts#L2), it also allows to pass another Response and TypeScript won't error.

So you basically just do

const response = await ViewModel({ request, context });
return response;

and make sure that in the happy path ViewModel returns a json(foo)

cerulean gulch
#

Hey thanks! Okay, so I see my mistake was in thinking that "returning" from the View Model was enough 🤦‍♂️ !!! I have to hand off its response to action/loader.

inland fiber
#

yeah. it's not entirely obvious so don't be too hard on yourself^^

#

as an aside, in our current app at work we put our models on context via getLoadContext and construct them with the currently logged in user
https://remix.run/docs/en/v1/api/conventions#loader-context

and then e.g.

export const action = async ({ request, context }) => {
  const vehicles = await context.models.vehicleModel.getMany(request);

  return json({ vehicles });
};

with the benefit that we don't have to import & pass context to them every time

Don't know your app's needs but thought that might be helpful

cerulean gulch
#

Oh that's an interesting idea; I am in the middle of building out auth for a new app and our current thinking was to expose auth stuff via context but yeah, I see your point.

#

In the example I share the View Model acts somewhat like middleware since we pass in each request/context.

#

We use it to offload all of the gross stuff and keep our loaders/actions clean 😅

inland fiber
#

Also, since I'm at it anyway 😅

My example above is not entirely correct. We do extraction before we pass the request to our models. E.g.

export const action = async ({ request, context }) => {
  const id = // somehow extract it from the request since it can come from anywhere ... formData, searchParam ...
  const user = await context.models.user.getById(id);

  return json({ user });
};
#

Our first idea was to offload everything to the model as well but that was too restrictive

#

And we do still expose auth stuff (= the currently logged in user which we call viewer) via context so that we can do something like

export const action = async ({ request, context }) => {
  requireAdmin(context.viewer);
};

Just some food for thought (:
We're also still looking for good patterns but are slowly settling on some things

cerulean gulch
#

Yep, know the feeling