DEV Community

Cover image for Upload Files to S3 Object Storage (or MinIo) with Apollo Server
Francisco Mendes
Francisco Mendes

Posted on

Upload Files to S3 Object Storage (or MinIo) with Apollo Server

In the past I've done articles on how to upload images to Cloudinary and S3 (or MinIO), but they were always REST articles. So this week I decided to make a small tutorial on how to create an API in GraphQL to upload files to S3.

And so that it is accessible to more people, that is, so that more people can do this tutorial, they can use MinIO. However, what will be used is the AWS SDK and not a MinIO client.

In addition to all this, at the end of the article I will share with you a link to the github repository with the code that will be shown in this article as well as a React application so you can try uploading a file.

Let's code

First let's install the necessary dependencies:

npm install express apollo-server-express graphql-upload aws-sdk

npm install --dev nodemon
Enter fullscreen mode Exit fullscreen mode

Now let's create some modules that will be used in our resolvers, starting first with the S3 bucket configuration:

// @/src/modules/bucket.js
import AWS from "aws-sdk";

export const bucket = "dev-gql-s3-bucket";

export const s3 = new AWS.S3({
  endpoint: "http://localhost:9000",
  accessKeyId: "ly1y6iMtYf",
  secretAccessKey: "VNcmMuDARGGstqzkXF1Van1Mlki5HGU9",
  sslEnabled: false,
  s3ForcePathStyle: true,
});
Enter fullscreen mode Exit fullscreen mode

As you may have noticed in our bucket configuration, our bucket name is dev-gql-s3-bucket.

One important thing I want to point out is that the S3's Access Key corresponds to the MinIo's root user, just as the S3's Secret Access Key corresponds to the root password.

Now we will need to create a function that will be used to upload the file:

// @/src/modules/streams.js
import stream from "stream";

import { bucket, s3 } from "./bucket.js";

export const createUploadStream = (key) => {
  const pass = new stream.PassThrough();
  return {
    writeStream: pass,
    promise: s3
      .upload({
        Bucket: bucket,
        Key: key,
        Body: pass,
      })
      .promise(),
  };
};
Enter fullscreen mode Exit fullscreen mode

With the modules created, we can start defining our GraphQL Schema:

// @/src/graphql/typeDefs.js
import { gql } from "apollo-server-express";

export const typeDefs = gql`
  scalar Upload

  type FileUploadResponse {
    ETag: String!
    Location: String!
    key: String!
    Key: String!
    Bucket: String!
  }

  type Query {
    otherFields: Boolean!
  }

  type Mutation {
    fileUpload(file: Upload!): FileUploadResponse!
  }
`;
Enter fullscreen mode Exit fullscreen mode

As you may have noticed in our schema, a scalar called Upload has been defined, which will be "mapped" to the implementation of the graphql-upload dependency.

With our schema defined, we can start working on our resolvers. First, let's import the necessary modules and dependencies:

// @/src/graphql/resolvers.js
import { ApolloError } from "apollo-server-express";
import { GraphQLUpload } from "graphql-upload";

import { createUploadStream } from "../modules/streams.js";

export const resolvers = {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

Then we'll map our scalar Upload with the graphql-upload implementation:

// @/src/graphql/resolvers.js
import { ApolloError } from "apollo-server-express";
import { GraphQLUpload } from "graphql-upload";

import { createUploadStream } from "../modules/streams.js";

export const resolvers = {
  Upload: GraphQLUpload,
  // ...
};
Enter fullscreen mode Exit fullscreen mode

Now we can start working on our mutation, which we go to our arguments to get the file:

// @/src/graphql/resolvers.js
import { ApolloError } from "apollo-server-express";
import { GraphQLUpload } from "graphql-upload";

import { createUploadStream } from "../modules/streams.js";

export const resolvers = {
  Upload: GraphQLUpload,
  Mutation: {
    fileUpload: async (parent, { file }) => {
      const { filename, createReadStream } = await file;

      // ...
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Then we will upload the file and it should be noted that the file/image key corresponds to the file name.

// @/src/graphql/resolvers.js
import { ApolloError } from "apollo-server-express";
import { GraphQLUpload } from "graphql-upload";

import { createUploadStream } from "../modules/streams.js";

export const resolvers = {
  Upload: GraphQLUpload,
  Mutation: {
    fileUpload: async (parent, { file }) => {
      const { filename, createReadStream } = await file;

      const stream = createReadStream();

      let result;

      try {
        const uploadStream = createUploadStream(filename);
        stream.pipe(uploadStream.writeStream);
        result = await uploadStream.promise;
      } catch (error) {
        console.log(
          `[Error]: Message: ${error.message}, Stack: ${error.stack}`
        );
        throw new ApolloError("Error uploading file");
      }

      return result;
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

With everything set up and defined, we can start working on our entry file. That is, we need to create our Apollo Server, start the server and implement the graphql upload middleware.

// @/src/main.js
import express from "express";
import { ApolloServer } from "apollo-server-express";
import { graphqlUploadExpress } from "graphql-upload";

import { typeDefs } from './graphql/typeDefs.js'
import { resolvers } from './graphql/resolvers.js'

async function startServer() {
  const server = new ApolloServer({
    typeDefs,
    resolvers,
  });

  await server.start();

  const app = express();

  app.use(graphqlUploadExpress());

  server.applyMiddleware({ app });

  await new Promise((r) => app.listen({ port: 4000 }, r));

  console.log(`šŸš€ Server ready at http://localhost:4000${server.graphqlPath}`);
}

startServer();
Enter fullscreen mode Exit fullscreen mode

While in our package.json just add the following properties:

{
  // ...
  "main": "main.js",
  "type": "module",
  "scripts": {
    "dev": "nodemon src/main.js"
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

As promised at the beginning of the article, here is the repository link.

Conclusion

As always, I hope you found it interesting. If you noticed any errors in this article, please mention them in the comments. šŸ§‘šŸ»ā€šŸ’»

Hope you have a great day! šŸ‘Œ

Discussion (0)