Working with PATs, GraphQL Mutations, and User Submitted Content with GraphCMS

Jamie Barton - Apr 2 '20 - - Dev Community

Let's take the following datamodel for a product that has many reviews.

type Product {
  id: ID!
  name: String!
  reviews: [Review!]!
}

type Review {
  id: ID!
  name: String!
  comment: String
}
Enter fullscreen mode Exit fullscreen mode

With GraphCMS, when you define this schema in your project, we automatically generate the applicable queries and mutations for the Product and Review models.

Quite often you will have interactive content that users can either subscribe to view, download, like or comment on. With the schema above, products can have many reviews, and a typical scenario would be to allow users to submit reviews directly from your website or follow up email.

However in the context of a headless CMS, allowing users to directly submit data to your GraphQL endpoint is risky, and gives curious users ability to inspect requests and do some naughty things you might not expect.

This is why GraphCMS has Permanent Auth Tokens, so only requests with a Authorization header will be let through. PATs can be scoped to queries, mutations, or both.

So how do we solve this problem? The answer is to create a custom endpoint or GraphQL layer to forward requests with the PAT.

If you open the GraphCMS API Playground, you'll see within the docs there is a generated mutation named createReview.

To create a product review, we'll need the name, comment and a productId that we can use to connect an existing Product entry.

Let's have a look at the mutation:

mutation ($productId: ID!, $name: String!, $comment: String) {
  createReview(data: {name: $name, comment: $comment, product: {connect: {id: $productId}}}) {
    id
    name
    comment
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let's create a quick function that uses graphql-request to send this mutation with the required variables.

We'll use Next.js + API routes to create our example, but the same concept applies to any framework + tech stack.

// In pages/api/submit-review.js
import { GraphQLClient } from 'graphql-request';

const client = new GraphQLClient(process.env.GRAPHCMS_URL, {
  headers: {
    Authorization: `Bearer ${process.env.GRAPHCMS_TOKEN}`,
  },
});

const mutation = `mutation ($productId: ID!, $name: String!, $comment: String) {
  createReview(data: {name: $name, comment: $comment, product: {connect: {id: $productId}}}) {
    id
    name
  }
}
`;

export default async (req, res) => {
  const { productId, name, comment } = JSON.parse(req.body);

  const variables = { productId, name, comment };

  try {
    const data = await client.request(mutation, variables);

    res.status(201).json(data);
  } catch (err) {
    res.send(err);
  }
};

Enter fullscreen mode Exit fullscreen mode

Then all that's left to do is create a form that submits the data to /api/submit-review.

// In components/ReviewForm.js
import { useState } from 'react';
import fetch from 'isomorphic-unfetch';
import { useForm, ErrorMessage } from 'react-hook-form';

const ReviewForm = ({ productId }) => {
  const [submitted, setSubmitted] = useState(false);
  const { formState, register, errors, handleSubmit } = useForm();
  const { isSubmitting } = formState;

  const onSubmit = async ({ name, comment }) => {
    try {
      const response = await fetch('/api/submit-review', {
        method: 'POST',
        body: JSON.stringify({ productId, name, comment }),
      });

      setSubmitted(true);
    } catch (err) {
      console.log(err)
    }
  };

  if (submitted) return <p>Review submitted. Thank you!</p>;

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input
          name="name"
          placeholder="Name"
          ref={register({ required: 'Name is required' })}
        />
        <ErrorMessage name="name" errors={errors} />
      </div>
      <div>
        <textarea name="comment" placeholder="Comment" ref={register} />
      </div>
      <div>
        <button type="submit" disabled={isSubmitting}>
          Create review
        </button>
      </div>
    </form>
  );
};

export default ReviewForm;
Enter fullscreen mode Exit fullscreen mode

You'll want to display any errors if there are any, but I'll leave that to you. 😉

All that's left to do is is render the <ReviewForm productId="..." /> component on our page and pass in the productId prop.

. . . . . . . . . . . . . . . . . . . .