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
}
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
}
}
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);
}
};
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;
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.