This week I had the unfortunate experience of trying to implement image uploading. I quickly realized that most tutorials are outdated, as Apollo Client stopped supporting image uploading with the release of Apollo Client 3. Adding to that, there wasn't much documentation for methods using TypeScript. I hope to add to that😇
You should be able to either intialize the repo with Ben Awads command npx create-graphql-api graphql-example
or you can also just clone this starter GitHub Repo I made. They are nearly the same, the GitHub repo doesn't have postgres though.
My main problem was also that I wanted to integrate the image uploading with my PostgresSQL database. This (hopefully) won't be a problem anymore.
Let's implement the backend first.
Backend
First, you have to create a Bucket on Google Cloud Platform. I just chose the default settings after giving it a name. You might have to create a project first, if you don't already have one. You can get $300 worth of credits too.
Next, create a service account. You need a service account to get service keys, which you in turn need to add into your app. Click on your service account, navigate to keys, press "Add key" and select JSON. You now have an API key! Insert this into your project.
Setup
For this app I want to create a blog post with an image. So in your post.ts
postresolver (or wherever your resolver to upload the image is), specify where the API-key is located:
const storage = new Storage({
keyFilename: path.join(
__dirname,
"/../../images/filenamehere.json"
),
});
const bucketName = "bucketnamehere";
Also make a const
for your bucket-name. You can see the name on Google Cloud Platform if you forgot.
To upload images with GraphQL, make sure to add [graphql-upload](https://github.com/jaydenseric/graphql-upload)
.
yarn add graphql-upload
Navigate to index.ts
. First disable uploads
from Apollo-client, since we are using graphql-upload
which conflicts with Apollo's own upload-property:
const apolloServer = new ApolloServer({
uploads: false, // disable apollo upload property
schema: await createSchema(),
context: ({ req, res }) => ({
req,
res,
redis,
userLoader: createUserLoader(),
}),
});
Next, also in index.ts
we need to use graphqlUploadExpress
. graphqlUploadExpress
is a middleware which allows us to upload files.
const app = express();
app.use(graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }));
apolloServer.applyMiddleware({
app
});
We can now write our resolver. First, let's upload a single file.
import { FileUpload, GraphQLUpload } from "graphql-upload";
@Mutation(() => Boolean)
async singleUpload(
//1
@Arg("file", () => GraphQLUpload)
{ createReadStream, filename }: FileUpload
) {
//2
await new Promise(async (resolve, reject) =>
createReadStream()
.pipe(
storage.bucket(bucketName).file(filename).createWriteStream({
resumable: false,
gzip: true,
})
)
//3
.on("finish", () =>
storage
.bucket(bucketName)
.file(filename)
.makePublic()
.then((e) => {
console.log(e[0].object);
console.log(
`https://storage.googleapis.com/${bucketName}/${e[0].object}`
);
})
)
.on("error", () => reject(false))
);
}
- The arguments are a little different. The Type-GraphQL type is
GraphQLUpload
which is fromgraphql-upload
. The TypeScript type is declared as{ createReadStream, filename }: FileUpload
withFileUpload
also being a type fromgraphql-upload
. - We await a new promise, and using a
createReadStream()
, wepipe()
to our bucket. Remember that we definedstorage
andbucketName
earlier to our own bucket-values. We can then create awriteStream
on our bucket. - When we are done uploading, we make the files public on our buckets and print the file uploaded. The public link to view the image uploaded is
[https://storage.googleapis.com/${bucketName}/${e[0].object
,](https://storage.googleapis.com/${bucketName}/${e[0].object,) so you would want to display this link on the front-end if needed. You can also just view the contents of your bucket on the GCP website.
Unfortunately, we can't verify that this works with the graphQL-playground, since it doesn't support file uploads. This is a job for Postman, which you can download here.
First, you need a suitable CURL-request for your resolver. Write this query into the GraphQL-playground:
mutation UploadImage($file: Upload!) {
singleUpload(file: $file)
}
In the top right corner you should press the "Copy CURL"-button. You should get something like this:
curl 'http://localhost:4000/graphql' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-alive' -H 'DNT: 1' -H 'Origin: http://localhost:4000' --data-binary '{"query":"mutation UploadImage($file: Upload!) {\n singleUpload(file: $file)\n}"}' --compressed
You only want to keep the highlighted part. This leaves me with
{"query":"mutation UploadImage($file: Upload!) {\n singleUpload(file: $file)\n}\n"}
Which is the operation I want. Now, back to Postman. Create a new POST-request and use the "Form-data" configuration under "Body":
key | value |
---|---|
operations | {"query":"mutation UploadImage($file: Upload!) {\n singleUpload(file: $file)\n}\n"} |
map | {"0":["variables.file"]} |
0 | GraphQL_Logo.svg.png |
press the "file"-configuration under the last row, "0". This will allow you to upload files.
Upload your desired file and send the request. The response should return "true". You can now view the image on Google Cloud!🔥
I will now show how to create a front-end for your application. If you want to save the image to a database, there is a section at the end on this.
Front-end
Setting up the front-end is a little more complicated. First, you have to setup your apollo-client.
//other unrelated imports up here
import { createUploadLink } from "apollo-upload-client";
new ApolloClient({
//@ts-ignore
link: createUploadLink({
uri: process.env.NEXT_PUBLIC_API_URL as string,
headers: {
cookie:
(typeof window === "undefined"
? ctx?.req?.headers.cookie
: undefined) || "",
},
fetch,
fetchOptions: { credentials: "include" },
}),
credentials: "include",
headers: {
cookie:
(typeof window === "undefined"
? ctx?.req?.headers.cookie
: undefined) || "",
},
//...cache:...
)}
My apollo client is a little overcomplicated because I needed to make sure that cookies worked😅 But the most important part is that you create an upload-link
with apollo rather than a normal http
-link.
Next, you have to implement the actual input-field where users can drop their file. My favorite fileinput-library is[react-dropzone](https://github.com/react-dropzone/react-dropzone)
. All react-dropzone needs is a div and an input😄
<div
{...getRootProps()}
>
<input accept="image/*" {...getInputProps()} />
<InputDrop></InputDrop>
</div>
You can control what happens when a user drops a file/chooses one with their useDropzone
hook:
const onDrop = useCallback(
([file]) => {
onFileChange(file);
},
[onFileChange]
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
When the user drops a file, I call onFileChange()
with the file that was just dropped in. Instead of onFileChange
you could also have an updater function called setFileToUpload()
using useState()
. Since I have also implemented cropping of my images, I need to process the image through some other functions before it's ready to be uploaded. But before this feature, I just uploaded the file directly.
I actually used Apollos useMutation()
-hook to implement uploading the image. First I define the mutation:
const uploadFileMutation = gql`
mutation UploadImage($file: Upload!) {
singleUpload(file: $file)
}
`;
We now need the before-mentioned hook from Apollo
const [uploadFile] = useUploadImageMutation();
Now, to actually upload the file, you can call this function. I am using this in the context of a form with Formik, so in my case it would be when the user submits the form.
await uploadFile(fileToUpload);
This should be enough to upload the image to your bucket. Let me know if you want the code to cropping, and I will write a little on that. For now, I deem it out of scope for this tutorial.
I promised to show how to store the image in a database, so here it is🤩
Integrating with a database and TypeORM on the backend
First you need to update your (in my case) Post.ts
-entity:
@Field()
@Column()
img!: string
I added a new Field where I save the image as a string. This is possible, since we are actually just saving the link to our image stored in our Google Bucket. Remember to run any migrations you might need. I am telling you since I forgot to at first😅
We then need to update our resolver on the backend:
@Mutation(() => Boolean)
@UseMiddleware(isAuth)
async createPost(
@Arg("file", () => GraphQLUpload)
{ createReadStream, filename }: FileUpload,
@Arg("input") input: PostInput,
@Ctx() { req }: MyContext
): Promise<Boolean> {
console.log("starts");
let imgURL = "";
const post = new Promise((reject) =>
createReadStream()
.pipe(
storage.bucket(bucketName).file(filename).createWriteStream({
resumable: false,
gzip: true,
})
)
.on("error", reject)
.on("finish", () =>
storage
.bucket(bucketName)
.file(filename)
.makePublic()
.then((e) => {
imgURL = `https://storage.googleapis.com/foodfinder-bucket/${e[0].object}`;
Post.create({
...input,
creatorId: req.session.userId,
img: imgURL,
}).save();
})
)
);
return true;
}
A lot of the code is the same as uploading a single file. I call Post.create({})
from TypeORM, which let's me save the new imgURL
which I get after uploading the image. I also save the current user's userId
, as well as the input from the form they just filled in. I get this from my PostInput
-class:
@InputType()
class PostInput {
@Field()
title: string;
@Field()
text: string;
}
This is just title
and text
strings, that is passed to our resolver.
The last step is to actually call the resolver. This time I will use graphQL code-gen, which I also have a tutorial about. In short, it generates fully-typed hooks corresponding to our GraphQL-mutation. Here is the mutation to create a post:
mutation CreatePost($input: PostInput!, $file: Upload!) {
createPost(input: $input, file: $file)
}
Takes the input of the post (title and text) aswell as a file. GraphQL codegen generates this hook, for the above mutation:
const [createPost] = useCreatePostMutation();
Simple as that! Remember to pass in the file and any other fields you might want to save:
await createPost({
variables: {
input: {
title: values.title,
text: values.text,
},
file: fileToUpload,
},
Now we are using our resolver to save the file and the other data from the form-input🔥
That's all done. If you want to know how to display the image, you can check out my other tutorial.
Conclusion
Great! Our users are now allowed to upload images to our application using Google Cloud Storage and GraphQL🎉🤩
I don't have a repo with this code isolated, but you can check it out on my side-project, FoodFinder in posts.ts
in the backend and create-post.tsx
for the frnot-end. As always, let me know if you have any questions😃
Top comments (2)
To the people who are here after having imports issue with the graphql-upload.
You have to update you're imports with deep-imports, something like this:
Previous:
New:
And add this to you're tsconfig file :
"allowJs": true,
"maxNodeModuleJsDepth": 10,
hello guys, if I want to save in a specific folder and not the root of the google cloud, would that be possible?