DEV Community

loading...
Cover image for Implementing image uploading with Type-GraphQL, Apollo and TypeORM

Implementing image uploading with Type-GraphQL, Apollo and TypeORM

lastnameswayne profile image Swayne ・7 min read

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";
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(),
    }),
  });
Enter fullscreen mode Exit fullscreen mode

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
    });
Enter fullscreen mode Exit fullscreen mode

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))
    );
  }
Enter fullscreen mode Exit fullscreen mode
  1. The arguments are a little different. The Type-GraphQL type is GraphQLUpload which is from graphql-upload. The TypeScript type is declared as { createReadStream, filename }: FileUpload with FileUpload also being a type from graphql-upload.
  2. We await a new promise, and using a createReadStream(), we pipe() to our bucket. Remember that we defined storage and bucketName earlier to our own bucket-values. We can then create a writeStream on our bucket.
  3. 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)
}
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

Which is the operation I want. Now, back to Postman. Create a new POST-request and use the "Form-data" configuration under "Body":

Postman
Fill in this data:

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:...
)}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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)
  }
`;
Enter fullscreen mode Exit fullscreen mode

We now need the before-mentioned hook from Apollo

const [uploadFile] = useUploadImageMutation();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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,
},
Enter fullscreen mode Exit fullscreen mode

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😃

Discussion (0)

pic
Editor guide