npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

remix-response

v1.0.2

Published

Semantic response helpers for your Remix app.

Downloads

8

Readme

remix-response

Semantic response helpers for your Remix app.

remix-response provides response helpers that wait on all promises to resolve before serializing the response.

Basic Usage

yarn add remix-response
import type { LoaderArgs } from "@remix-run/node";
import { ok } from 'remix-response';

const wait = (delay: number) => new Promise((r) => setTimeout(r, delay));
const fetchListings = (search: string) => wait(600).then(() => []);
const fetchRecommendations = (user: unknown) => wait(300).then(() => []);

export const loader = async ({ request, context }: LoaderArgs) => {
  const listings = fetchListings(request.url);
  const recommendations = fetchRecommendations(context.user);

  return ok({
    listings, // Promise<[]>
    recommendations, // Promise<[]>
  });
};

export default function MyRouteComponent() {
    const data = useLoaderData<typeof loader>(); // { listings: [], recommendations: [] }
    // ...
}

Don't go chasin' waterfalls

The simplest way fetch data in a remix loader is to use an async function and unwrap every promise with await.

import type { LoaderArgs } from "@remix-run/node";
import { json } from "@remix-run/node";

const wait = (delay: number) => new Promise((r) => setTimeout(r, delay));
const fetchListings = (search: string) => wait(600).then(() => []);
const fetchRecommendations = (user: unknown) => wait(300).then(() => []);

export const loader = async ({ request, context }: LoaderArgs) => {
  const listings = await fetchListings(request.url);
  const recommendations = await fetchRecommendations(context.user);

  return json({
    listings,
    recommendations,
  });
};

However, if we need to fetch data from multiple independent sources this can slow down the loader response since fetchRecommendations doesn't start until after the fetchListings request has been completed. A better approach would be to delay waiting until all the fetchs have been initiated.

export const loader = async ({ request, context }: LoaderArgs) => {
-  const listings = await fetchListings(request.url);
+  const listings = fetchListings(request.url);
-  const recommendations = await fetchRecommendations(context.user);
+  const recommendations = fetchRecommendations(context.user);

  return json({
-    listings,
+    listings: await listings,
-    recommendations,
+    recommendations: await recommendations,
  });
};

This change improves the time it takes to run the loader function because now all the fetches are run in parallel and we only need to wait for the longest fetch to complete.

remix-response can simplifiy things a bit further by automatically awaiting any promises provided to the top level object before serializing the response.

This is similar to the behavior of Promise.all but it preserves the object shape and keys similar to RSVP.hash or bluebird's Promise.props.

- import { json } from "@remix-run/node";
+ import { ok } from 'remix-response';

export const loader = async ({ request, context }: LoaderArgs) => {
  const listings = fetchListings(request.url);
  const recommendations = fetchRecommendations(context.user);

-  return json({
+  return ok({
-    listings: await listings,
+    listings,
-    recommendations: await recommendations,
+    recommendations,
  });
};

Errors

When returning a response, if any of the promises reject the response will have a 500 status code. The data object will contain all of the properites with an object similar to Promise.allSettled indicating if the promises are fulfilled or rejected and the value/reason. This object can be used in your ErrorBoundary component to render the appropriate error message.

import type { LoaderArgs } from "@remix-run/node";
import { ok } from 'remix-response';

const wait = (delay: number) => new Promise((r) => setTimeout(r, delay));
const fetchListings = (search: string) => wait(600).then(() => []);
const fetchRecommendations = (user: unknown) => wait(300).then(() => []);

export const loader = async ({ request, context }: LoaderArgs) => {
  const listings = fetchListings(request.url);
  const recommendations = fetchRecommendations(context.user);

  return ok({
    listings, // Promise<[]>
    recommendations, // Promise<[]>
    ohNo: Promise.reject('oops!'),
  });
};

export function ErrorBoundary() {
  const error = useRouteError();
  // {
  //   status: 500,
  //   statusText: 'Server Error',
  //   data: {
  //     listings: { status: 'fulfilled', value: [] },
  //     recommendations: { status: 'fulfilled', value: [] },
  //     ohNo: { status: 'rejected', reason: 'oops' },
  //   }
  // }

    return (
      <div>
        <h1>
          {error.status} {error.statusText}
        </h1>
        <pre>{JSON.stringify(error.data, null, 2)}</pre>
      </div>
    );
}

If a response is thrown in the loader this indicates an error. Thrown responses will always keep their original status even if a promise rejects. Unlike a returned response, thown responses always use a settled object format with the status and value/reason. This is to ensure the shape will always be consistent in the ErrorBoundary component.

import type { LoaderArgs } from "@remix-run/node";
import { notFound } from 'remix-response';

const wait = (delay: number) => new Promise((r) => setTimeout(r, delay));
const fetchListings = (search: string) => wait(600).then(() => []);
const fetchRecommendations = (user: unknown) => wait(300).then(() => []);

export const loader = async ({ request, context }: LoaderArgs) => {
  const listings = fetchListings(request.url);
  const recommendations = fetchRecommendations(context.user);

  throw notFound({
    listings, // Promise<[]>
    recommendations, // Promise<[]>
  });
};

export function ErrorBoundary() {
  const error = useRouteError();
  // {
  //   status: 404,
  //   statusText: 'Not Found',
  //   data: {
  //     listings: { status: 'fulfilled', value: [] },
  //     recommendations: { status: 'fulfilled', value: [] },
  //   }
  // }

  return null;
}

API

Members

import { created } from 'remix-response';
export const action = async () => {
  return created({
    status: 'new',
    id: Promise.resolve(1),
  });
};
import { created } from 'remix-response';
export const action = async () => {
  return noContent();
};
import { resetContent } from 'remix-response';
export const loader = async () => {
  return resetContent({
    form: {},
    id: Promise.resolve(1),
  });
};
import { partialContent } from 'remix-response';
export const loader = async () => {
  return partialContent({
    title: 'RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1',
    id: Promise.resolve(2616),
  });
};
import { movedPermanently } from 'remix-response';
export const loader = async () => {
  return movedPermanently('https://www.example.com/');
};
import { found } from 'remix-response';
export const action = async () => {
  return found('https://www.example.com/');
};
import { seeOther } from 'remix-response';
export const action = async () => {
  return seeOther('https://www.example.com/');
};
import { notModified } from 'remix-response';
export const loader = async ({ request }: LoaderArgs) => {
  if (request.headers.get('If-Modified-Since') === 'Wed, 21 Oct 2015 07:28:00 GMT') {
    return notModified(request.url);
  }
};
import { temporaryRedirect } from 'remix-response';
export const action = async () => {
  return temporaryRedirect('https://www.example.com/');
};
import { permanentRedirect } from 'remix-response';
export const action = async () => {
  return permanentRedirect('https://www.example.com/');
};
import type { ActionArgs } from &quot;@remix-run/node&quot;;
import { badRequest } from 'remix-response';
export async function action({ request }: ActionArgs) {
  return badRequest({
    form: request.formData(),
    errors: Promise.resolve({name: 'missing'}),
  });
};
import type { ActionArgs } from &quot;@remix-run/node&quot;;
import { unauthorized } from 'remix-response';
export async function action({ request }: ActionArgs) {
  return unauthorized({
    form: request.formData(),
    errors: Promise.resolve({user: 'missing'}),
  });
};
import type { ActionArgs } from &quot;@remix-run/node&quot;;
import { forbidden } from 'remix-response';
export async function action({ request }: ActionArgs) {
  return forbidden({
    form: request.formData(),
    errors: Promise.resolve({user: 'missing'}),
  });
};
import { notFound } from 'remix-response';
export async function loader() {
  return notFound({
    recommendations: []
    fromTheBlog: Promise.resolve([]),
  });
};
import { methodNotAllowed } from 'remix-response';
export async function action() {
  return methodNotAllowed({
    allowedMethods: Promise.resolve(['GET', 'POST']),
  });
};
import { notAcceptable } from 'remix-response';
export async function action() {
  return notAcceptable({
    allowedLanguage: Promise.resolve(['US_en', 'US_es']),
  });
};
import { conflict } from 'remix-response';
export async function action() {
  return conflict({
    error: Promise.resolve({ id: 'duplicate id' }),
  });
};
import { gone } from 'remix-response';
export async function action() {
  return gone({
    error: Promise.resolve('resource deleted'),
  });
};
import { preconditionFailed } from 'remix-response';
export async function action() {
  return preconditionFailed({
    modifiedSince: Promise.resolve(Date.now()),
  });
};
import { expectationFailed } from 'remix-response';
export async function action() {
  return expectationFailed({
    error: Promise.resolve('Content-Length is too large.'),
  });
};
import { teapot } from 'remix-response';
export async function action() {
  return teapot({
    error: Promise.resolve('🚫☕'),
  });
};
import { preconditionFailed } from 'remix-response';
export async function action() {
  return preconditionFailed({
    error: Promise.resolve('Missing If-Match header.'),
  });
};
import { tooManyRequests } from 'remix-response';
export async function action() {
  return tooManyRequests({
    retryIn: Promise.resolve(5 * 60 * 1000),
  });
};
import { serverError } from 'remix-response';
export async function loader() {
  throw serverError({
    error: Promise.resolve('Unable to load resouce.'),
  });
};
import { notImplemented } from 'remix-response';
export async function loader() {
  throw notImplemented({
    error: Promise.resolve('Unable to load resouce.'),
  }, {
    headers: { 'Retry-After': 300 }
  });
};
import { serviceUnavailable } from 'remix-response';
export async function loader() {
  throw serviceUnavailable({
    error: Promise.resolve('Unable to load resouce.'),
  }, {
    headers: { 'Retry-After': 300 }
  });
};

Constants

import { ok } from 'remix-response';
export const loader = async () => {
  return ok({
    hello: 'world',
    promise: Promise.resolve('result'),
  });
};

ok

import { created } from 'remix-response';
export const action = async () => {
  return created({
    status: 'new',
    id: Promise.resolve(1),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

created

import { created } from 'remix-response';
export const action = async () => {
  return noContent();
};

Kind: global variable

| Param | Description | | --- | --- | | init? | An optional RequestInit configuration object. |

noContent

import { resetContent } from 'remix-response';
export const loader = async () => {
  return resetContent({
    form: {},
    id: Promise.resolve(1),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

resetContent

import { partialContent } from 'remix-response';
export const loader = async () => {
  return partialContent({
    title: 'RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1',
    id: Promise.resolve(2616),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

partialContent

import { movedPermanently } from 'remix-response';
export const loader = async () => {
  return movedPermanently('https://www.example.com/');
};

Kind: global variable

| Param | Description | | --- | --- | | url | A url to redirect the request to |

movedPermanently

import { found } from 'remix-response';
export const action = async () => {
  return found('https://www.example.com/');
};

Kind: global variable

| Param | Description | | --- | --- | | url | A url to redirect the request to |

found

import { seeOther } from 'remix-response';
export const action = async () => {
  return seeOther('https://www.example.com/');
};

Kind: global variable

| Param | Description | | --- | --- | | url | A url to redirect the request to |

seeOther

import { notModified } from 'remix-response';
export const loader = async ({ request }: LoaderArgs) => {
  if (request.headers.get('If-Modified-Since') === 'Wed, 21 Oct 2015 07:28:00 GMT') {
    return notModified(request.url);
  }
};

Kind: global variable

| Param | Description | | --- | --- | | url | A url to redirect the request to |

notModified

import { temporaryRedirect } from 'remix-response';
export const action = async () => {
  return temporaryRedirect('https://www.example.com/');
};

Kind: global variable

| Param | Description | | --- | --- | | url | A url to redirect the request to |

temporaryRedirect

import { permanentRedirect } from 'remix-response';
export const action = async () => {
  return permanentRedirect('https://www.example.com/');
};

Kind: global variable

| Param | Description | | --- | --- | | url | A url to redirect the request to |

permanentRedirect

import type { ActionArgs } from &quot;@remix-run/node&quot;;
import { badRequest } from 'remix-response';
export async function action({ request }: ActionArgs) {
  return badRequest({
    form: request.formData(),
    errors: Promise.resolve({name: 'missing'}),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

badRequest

import type { ActionArgs } from &quot;@remix-run/node&quot;;
import { unauthorized } from 'remix-response';
export async function action({ request }: ActionArgs) {
  return unauthorized({
    form: request.formData(),
    errors: Promise.resolve({user: 'missing'}),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

unauthorized

import type { ActionArgs } from &quot;@remix-run/node&quot;;
import { forbidden } from 'remix-response';
export async function action({ request }: ActionArgs) {
  return forbidden({
    form: request.formData(),
    errors: Promise.resolve({user: 'missing'}),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

forbidden

import { notFound } from 'remix-response';
export async function loader() {
  return notFound({
    recommendations: []
    fromTheBlog: Promise.resolve([]),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

notFound

import { methodNotAllowed } from 'remix-response';
export async function action() {
  return methodNotAllowed({
    allowedMethods: Promise.resolve(['GET', 'POST']),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

methodNotAllowed

import { notAcceptable } from 'remix-response';
export async function action() {
  return notAcceptable({
    allowedLanguage: Promise.resolve(['US_en', 'US_es']),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

notAcceptable

import { conflict } from 'remix-response';
export async function action() {
  return conflict({
    error: Promise.resolve({ id: 'duplicate id' }),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

conflict

import { gone } from 'remix-response';
export async function action() {
  return gone({
    error: Promise.resolve('resource deleted'),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

gone

import { preconditionFailed } from 'remix-response';
export async function action() {
  return preconditionFailed({
    modifiedSince: Promise.resolve(Date.now()),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

preconditionFailed

import { expectationFailed } from 'remix-response';
export async function action() {
  return expectationFailed({
    error: Promise.resolve('Content-Length is too large.'),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

expectationFailed

import { teapot } from 'remix-response';
export async function action() {
  return teapot({
    error: Promise.resolve('🚫☕'),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

teapot

import { preconditionFailed } from 'remix-response';
export async function action() {
  return preconditionFailed({
    error: Promise.resolve('Missing If-Match header.'),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

preconditionRequired

import { tooManyRequests } from 'remix-response';
export async function action() {
  return tooManyRequests({
    retryIn: Promise.resolve(5 * 60 * 1000),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

tooManyRequests

import { serverError } from 'remix-response';
export async function loader() {
  throw serverError({
    error: Promise.resolve('Unable to load resouce.'),
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

serverError

import { notImplemented } from 'remix-response';
export async function loader() {
  throw notImplemented({
    error: Promise.resolve('Unable to load resouce.'),
  }, {
    headers: { 'Retry-After': 300 }
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

notImplemented

import { serviceUnavailable } from 'remix-response';
export async function loader() {
  throw serviceUnavailable({
    error: Promise.resolve('Unable to load resouce.'),
  }, {
    headers: { 'Retry-After': 300 }
  });
};

Kind: global variable

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |

ok

import { ok } from 'remix-response';
export const loader = async () => {
  return ok({
    hello: 'world',
    promise: Promise.resolve('result'),
  });
};

Kind: global constant

| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |