Coder Social home page Coder Social logo

remix-graphql's Introduction

remix-graphql

Remix and GraphQL can live together in harmony โค๏ธ This package contains basic utility functions that can help you with that.

To be more speciic, here's what the latest version of remix-graphql can help you with:

  • Handling loader and action requests using GraphQL queries and mutations
    • You can define a local schema and resolvers to handle the request
    • You can also run perform GraphQL requests again a remote API
  • Setting up a GraphQL API as a resource route

And here are some cool ideas what it might do as well in the future:

  • Batching queries from multiple loaders into a single API request

Contents

Installing

You can install remix-graphql with your preferred package manager. It depends on the graphql package, so make sure to also have that installed.

# Using `npm`
npm install graphql remix-graphql
# Or using `yarn`
yarn add graphql remix-graphql

It also lists some of the Remix-packages as peer dependencies. (If you used the Remix CLI to setup your project, you most likely have them installed already.) If you get unexpected errors, double check that the following are installed:

  • @remix-run/dev
  • @remix-run/react
  • @remix-run/serve
  • remix

How to import from remix-graphql

This module not indended to be used in a browser environment, it only works on the server. You can force the Remix compiler to never ever include stuff from remix-graphql in the client bundle by importing from a file with a .server.js (or .server.ts) extension.

// This will not work and will actually throw an error:
import { anything } from "remix-graphql";

// Do this instead:
import { anything } from "remix-graphql/index.server";

Defining your schema

remix-graphql keeps it simple and let's you decide on the best way to define your GraphQL schema. In all places where you need to "pass your schema to remix-graphql", the respective function expects a GraphQLSchema object.

That means all of the following approached work to define a schema:

  • Using the GraphQLSchema class from the graphql package (obviously...)
  • Defining the schema using the SDL, defining resolver functions in an object and merging both with makeExecutableSchema (from @graphql-tools/schema)
  • Using nexus and makeSchema

We recommend exporting the schema from a file, e.g. app/graphql/schema.server.ts. By using the .server.ts extension you make sure that none of this code will end up being shipped to the browser. (This is a hint to the Remix compiler that it should ignore this module when building the browser bundle.)

Handle loader and action requests with GraphQL

Both loaders and actions are just simple functions that return a Response given a Request. With remix-graphql you can use GraphQL to process this request! Here's a complete and working example of how it works:

// app/routes/index.tsx
import type { GraphQLError } from "graphql";
import { Form } from "remix";
import type { ActionFunction, LoaderFunction } from "@remix-run/node";;
import { processRequestWithGraphQL } from "remix-graphql/index.server";

// Import your schema from whereever you export it
import { schema } from "~/graphql/schema";

const ALL_POSTS_QUERY = /* GraphQL */ `
  query Posts($limit: Int) {
    posts(limit: $limit) {
      id
      title
      likes
      author {
        name
      }
    }
  }
`;

export const loader: LoaderFunction = (args) =>
  processRequestWithGraphQL({
    // Pass on the arguments that Remix passes to a loader function.
    args,
    // Provide your schema.
    schema,
    // Provide a GraphQL operation that should be executed. This can also be a
    // mutation, it is named `query` to align with the common naming when
    // sending GraphQL requests over HTTP.
    query: ALL_POSTS_QUERY,
    // Optionally provide variables that should be used for executing the
    // operation. If this is not passed, `remix-graphql` will derive variables
    // from...
    // - ...the route params.
    // - ...the submitted `formData` (if it exists).
    variables: { limit: 10 },
    // Optionally pass an object with properties that should be included in the
    // execution context.
    context: {},
    // Optionally pass a function to derive a custom HTTP status code for a
    // successfully executed operation.
    deriveStatusCode(
      // The result of the execution.
      executionResult: ExecutionResult,
      // The status code that would be returned by default, i.e. of the
      // `deriveStatusCode` function is not passed.
      defaultStatusCode: number
    ) {
      return defaultStatusCode;
    },
  });

const LIKE_POST_MUTATION = /* GraphQL */ `
  mutation LikePost($id: ID!) {
    likePost(id: $id) {
      id
      likes
    }
  }
`;

// The `processRequestWithGraphQL` function can be used for both loaders and
// actions!
export const action: ActionFunction = (args) =>
  processRequestWithGraphQL({ args, schema, query: LIKE_POST_MUTATION });

export default function IndexRoute() {
  const { data } = useLoaderData<LoaderData>();
  if (!data) {
    return "Ooops, something went wrong :(";
  }

  return (
    <main>
      <h1>Blog Posts</h1>
      <ul>
        {data.posts.map((post) => (
          <li key={post.id}>
            {post.title} (by {post.author.name})
            <br />
            {post.likes} Likes
            <Form method="post">
              {/* `remix-graphql` will automatically transform all posted 
                  form data into variables of the same name for the GraphQL
                  operation */}
              <input hidden name="id" value={post.id} />
              <button type="submit">Like</button>
            </Form>
          </li>
        ))}
      </ul>
    </main>
  );
}

type LoaderData = {
  data?: {
    posts: {
      id: string;
      title: string;
      likes: number;
      author: { name: string };
    }[];
  };
  errors?: GraphQLError[];
};

Automated type generation

Hidden at the end of the example above you see that the data returned from the loader function had to be typed by hand. Since GraphQL is strongly typed, you can automate this if you want to!

First, you need to generate the introspection data as JSON from your schema and store it in a local file. For that you can create a simple script like this:

// app/graphql/introspection.{js,ts}
import fs from "fs";
import { introspectionFromSchema } from "graphql";
import path from "path";
import { schema } from "./schema";

fs.writeFileSync(
  path.join(__dirname, "introspection.json"),
  JSON.stringify(introspectionFromSchema(schema))
);

Usually you don't want to commit the generated JSON file to version control, so we recommend to add it to your .gitignore file.

To make running this script easier, create a simple NPM script for it in your package.json:

{
  "scripts": {
    // If you created the script with JavaScript
    "introspection": "node app/graphql/introspection.js",
    // If you created the script with TypeScript (make sure to install
    // `esbuild-register` as dev-dependency in this case)
    "introspection": "node --require esbuild-register app/graphql/introspection.ts"
  }
}

To actually generate types from your queries and mutations we recommend using GraphQL Code Generator. For that you need to install a couple of dependencies:

# Using `npm`
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
# Or using `yarn`
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations

Almost there! Now create a config file named codegen.yml in the root of your project that contains the following:

overwrite: true
# The path where the previously generated introspection data is stored
schema: "app/graphql/introspection.json"
# A glob that matches all files that contain operation definitions
documents: "app/routes/**/*.{ts,tsx}"
generates:
  # This is the path where the generated types will be stored
  app/graphql/types.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
    config:
      skipTypename: true

Now you can finally generate the types! For convenience, add another NPM script:

{
  "scripts": {
    "introspection": "node --require esbuild-register app/graphql/introspection.ts",
    "codegen": "npm run introspection && graphql-codegen --config codegen.yml"
  }
}

Running npm run codegen (or yarn codegen) will now automatically create types for the returned data for all queries and mutations. (Side-note: It's also a great way to validate if all your operations are valid against your schema!)

One more thing: Noticed the /* GraphQL */ comment we included before the strings that contain queries and mutations in the example above? This is important! It's a hint to @graphql-codegen that this string should be parsed as GraphQL. Without it you won't get any types for the operation defined within the string.

The example above could now be modified like this:

// Add this import...
import type { PostsQuery } from "~/graphql/types";

// ...and change the `LoaderData` type like this:
type LoaderData = { data?: PostsQuery; errors?: GraphQLError[] };

Send requests to a remote GraphQL API

Maybe you don't want to write your GraphQL API as part of your Remix app, or you want to use a third-party GraphQL API like GitHubs public API. In both cases remix-graphql helps you with that!

// app/routes/$username.tsx
import type { GraphQLError } from "graphql";
import type { LoaderFunction } from "@remix-run/node";
import { sendGraphQLRequest } from "remix-graphql/index.server";

const LOAD_USER_QUERY = /* GraphQL */ `
  query LoadUser($username: String!) {
    user(login: $username) {
      name
    }
  }
`;

export const loader: LoaderFunction = (args) =>
  sendGraphQLRequest({
    // Pass on the arguments that Remix passes to a loader function.
    args,
    // Provide the endpoint of the remote GraphQL API.
    endpoint: "https://api.github.com/graphql",
    // Optionally add headers to the request.
    headers: { authorization: `Bearer ${process.env.GITHUB_TOKEN}` },
    // Provide the GraphQL operation to send to the remote API.
    query: LOAD_USER_QUERY,
    // Optionally provide variables that should be used for executing the
    // operation. If this is not passed, `remix-graphql` will derive variables
    // from...
    // - ...the route params.
    // - ...the submitted `formData` (if it exists).
    // That means the following is the default and could also be ommited.
    variables: args.params,
  });

export default function UserRoute() {
  const { data } = useLoaderData<LoaderData>();
  if (!data) {
    return "Ooops, something went wrong :(";
  }
  if (!data.user) {
    return "User not found :(";
  }
  return <h1>{data.user.name}</h1>;
}

type LoaderData = {
  data?: {
    user: {
      name: string | null;
    } | null;
  };
  errors?: GraphQLError[];
};

If you want to do more stuff in your loader than just a single GraphQL query, you can totally do that! The function sendGraphQLRequest will return the Response object from the fetch-request to the remote API, so you can do with that whatever you need in your loader.

import { json } from "remix";
import type { LoaderFunction } from "@remix-run/node";
import { sendGraphQLRequest } from "remix-graphql/index.server";

const LOAD_USER_QUERY = /* GraphQL */ `
  query LoadUser($username: String!) {
    user(login: $username) {
      name
    }
  }
`;

export const loader: LoaderFunction = (args) => {
  try {
    const loadUserRes = await sendGraphQLRequest({
      args,
      endpoint: "https://api.github.com/graphql",
      headers: { authorization: `Bearer ${process.env.GITHUB_TOKEN}` },
      query: LOAD_USER_QUERY,
    }).then((res) => res.json());

    /* You can do any additional stuff here...  */
    const otherStuff = 42;

    return json({ username: loadUserRes.data.user.name, otherStuff });
  } catch {
    throw new Response("Something went wrong while loading the data :(");
  }
};

Set up a GraphQL API in a Remix app

You can create a dedicated endpoint for your GraphQL API using resource routes in Remix. All you need to do is create a route (e.g. app/routes/graphql.ts) and paste the following code. By using both a loader and an action your endpoint supports both GET and POST requests!

// app/routes/graphql.ts
import {
  getActionFunction,
  getLoaderFunction,
} from "remix-graphql/index.server";
import type { DeriveStatusCodeFunction } from "remix-graphql/index.server";

// Import your schema from whereever you export it
import { schema } from "~/graphql/schema";

// Handles GET requests
export const loader = getLoaderFunction({
  // Provide your schema.
  schema,
  // Optionally pass an object with properties that should be included in the
  // execution context.
  context: {},
  // Optionally pass a function to derive a custom HTTP status code for a
  // successfully executed operation.
  deriveStatusCode,
});

// Handles POST requests
export const action = getActionFunction({
  // Provide your schema.
  schema,
  // Optionally pass an object with properties that should be included in the
  // execution context.
  context: {},
  // Optionally pass a function to derive a custom HTTP status code for a
  // successfully executed operation.
  deriveStatusCode,
});

// This function equals the default behaviour.
const deriveStatusCode: DeriveStatusCodeFunction = (
  // The result of the execution.
  executionResult,
  // The status code that would be returned by default, i.e. of the
  // `deriveStatusCode` function is not passed.
  defaultStatusCode
) => defaultStatusCode;

Context

When defining a schema and writing resolvers, it's common to provide a context- object. All functions exported by remix-graphql accept an optional property context in the arguments object. When passed, it must be an object. All of its properties will be included in the context object passed to your resolvers.

remix-graphql also exports a Context type that contains all properties that are added to this context objects for execution. This type accepts an optional generic by which you can add any custom properties to your context object.

import type { PrismaClient } from "@prisma/client";
import type { Context } from "remix-graphql/index.server";

type ContextWithDatabase = Context<{ db: PrismaClient }>;

The following subsections highlight all properties that are added to the context object by remix-graphql.

request

This is the Request object that is passed to a loader- or action-function in Remix. It will always be part of the context object.

redirect

When handling loaders or actions in UI routes, a common pattern in Remix is redirection. (Remix even provides a redirect utility function that can be returned from any loader- or action-function.) In remix-graphql you can achieve this by using the redirect function that is provided in the context object.

This function has the following signature:

function redirect(
  // The URL for redirection
  url: string,
  // Optionally header values to include in the HTTP response
  headers?: HeadersInit
): void;

Note that this function is only part of the context object when handling GraphQL requests in UI routes, i.e. when using processRequestWithGraphQL. It is NOT part of the context object when handling GraphQL requests in a resource route, i.e. when using getActionFunction or getLoaderFunction.

remix-graphql's People

Contributors

cerchie avatar cliffordfajardo avatar thomasheyenbrock avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

remix-graphql's Issues

remix-graphql ships `fs` to the browser

Hi, I'm trying to use remix-graphql but it explodes the bundle size (from 150kB to 650kB).

To reproduce:

  1. use create-remix to generate a new Remix app
  2. use sendGraphQLRequest from remix-graphql in the loader

As a workaround, I re-export everything from remix-graphql in a remix-graphql.server.ts file so it will be excluded from the client bundle.

createLoaderFunction does not exist

Describe the bug

The documented createLoaderFunction on README does not exist on the code.

Your Example Website or App

https://github.com/anonrig/bookclub

Steps to Reproduce the Bug or Issue

The documented createLoaderFunction on README does not exist on the code.

Expected behavior

The documented createLoaderFunction on README does not exist on the code.

Screenshots or Videos

No response

Javascript runtime version

18

Deploy Target

Cloudflare Workers

Operating System

macOS

Additional context

No response

Error thrown when consuming graphql

Describe the bug

I'm running into an error once I set up remix and try to consume graphql with this package.

Your Example Website or App

https://github.com/Cerchie/remix-graphql-error

Steps to Reproduce the Bug or Issue

To replicate the issue I'm running into, on MacOS, I ran:

~ % npx create-remix@latest
Need to install the following packages:
  create-remix@latest
Ok to proceed? (y) y
npm WARN deprecated [email protected]: See https://github.com/lydell/source-map-url#deprecated
npm WARN deprecated [email protected]: https://github.com/lydell/resolve-url#deprecated
npm WARN deprecated [email protected]: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.
npm WARN deprecated [email protected]: See https://github.com/lydell/source-map-resolve#deprecated
npm WARN deprecated [email protected]: Please see https://github.com/lydell/urix#deprecated
? Where would you like to create your app? ./remix-experiment
? What type of app do you want to create? Just the basics
? Where do you want to deploy? Choose Remix if you're unsure; it's easy to 
change deployment targets. Remix App Server
? Do you want me to run `npm install`? Yes

I then ran npm run dev and the app worked.

The next thing I did was add a file called $username.tsx to the /routes folder, and copy-pasted the code for consuming graphql from the readme in this repository. I got this error:

 File changed: app/routes/$username.tsx
๐Ÿ’ฟ Rebuilding...
๐Ÿ’ฟ Rebuilt in 68ms
Error: Did you forget to run `remix setup` for your platform?
    at Object.<anonymous> (/Users/luciacerchie/remix-experiment/node_modules/remix/index.js:22:7)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/Users/luciacerchie/remix-experiment/node_modules/remix-graphql/dist/index.js:35:21)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
GET / 500 - - 68.833 ms
^C

It's odd because my repository doesn't have that script as generated.

Expected behavior

I expected to see a 401 without the GitHub token, or expecting a 200 without it, but I'm seeing the above error (500) in my terminal instead.

Screenshots or Videos

No response

Javascript runtime version

v16.13.1

Deploy Target

No response

Operating System

MacOS Monterey

Additional context

No response

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.