In this tutorial, we’ll go over how to handle file uploads in GraphQL by building a full-stack app. This tutorial will be divided into two main sections: building the GraphQL API, and creating the frontend app. The GraphQL API will be built using Apollo Server and the frontend app will be built with Vue.js and Vue Apollo.
This tutorial assumes you are comfortable with GraphQL and Vue. For more guidance, you can learn from this GraphQL with Node tutorial, and this tutorial that builds a blog with Vue, GraphQL, and Apollo Client.
For the purpose of this tutorial, we’ll be building a photo album app, where users will be able to upload as well as view their photos. All the photos will be uploaded directly to Cloudinary. Below is a quick demo of the final app:
Before we dive into code, let’s make sure we have our Cloudinary key in place. If you don’t already have an account with them, you can signup for free. Otherwise, login to your dashboard where you would see your account details along with your keys.
Now, let’s start building the GraphQL server. First, let’s create our project directory:
$ mkdir graphql-vue-photo-upload && cd graphql-vue-photo-upload
$ mkdir server && cd server
$ npm init -y
All the GraphQL API related code will be inside the server
directory. Next, let’s install the necessary dependencies for our GraphQL server:
$ npm install graphql apollo-server cloudinary dotenv
Addition to installing Apollo Server and it dependence, we also install the Cloudinary Node.js library and a package to read environment variables.
Once that’s done installing, we can start building the GraphQL server. Create a new src
directory and create a new index.js
file inside it, then add the following code in it:
// server/src/index.js
const { ApolloServer } = require('apollo-server')
require('dotenv').config()
const typeDefs = require('./schema')
const resolvers = require('./resolvers')
const server = new ApolloServer({
typeDefs,
resolvers
})
server.listen().then(({ url }) => console.log(`Server ready at ${url}`))
Next, we need to create the schema and resolvers which our GraphQL server references. We’ll start with creating the schema. Create a schema.js
inside src
directory and paste the following code into it:
// server/src/schema.js
const { gql } = require('apollo-server')
const typeDefs = gql`type Photo {
filename: String!
path: String!
}
type Query {
allPhotos: [Photo]
}
type Mutation {
uploadPhoto(photo: Upload!): Photo!
}`
module.exports = typeDefs
Here, we define a Photo
type that comprises of two fields: the filename of the photo and the path to the actual photo. Then we define a single query allPhotos
for fetching all uploaded photos. Lastly, we have a mutation for uploading a photo. The uploadPhoto
mutation accepts a single argument, which is the photo that is to be uploaded. The argument is of the scalar type Upload
, which is made available to us my Apollo Server, since it has built-in support of file upload. The mutation will return the uploaded photo.
The next, thing to do is create the resolvers. Still inside src
directory, create a resolvers.js
and add the code below in it:
// server/src/resolvers.js
const cloudinary = require('cloudinary').v2
cloudinary.config({
cloud_name: process.env.CLOUD_NAME,
api_key: process.env.API_KEY,
api_secret: process.env.API_SECRET
})
const photos = []
const resolvers = {
Query: {
allPhotos () {
return photos
}
},
Mutation: {
async uploadPhoto (parent, { photo }) {
const { filename, createReadStream } = await photo
try {
const result = await new Promise((resolve, reject) => {
createReadStream().pipe(
cloudinary.uploader.upload_stream((error, result) => {
if (error) {
reject(error)
}
resolve(result)
})
)
})
const newPhoto = { filename, path: result.secure_url }
photos.push(newPhoto)
return newPhoto
} catch (err) {
console.log(err)
}
}
}
}
module.exports = resolvers
First, we pull in the Cloudinary library and configured it will our credentials, which are getting from the environment variables. Then we create an empty array, which will hold our photos. Next, we define the resolver for the allPhotos
query, which returns the photos array.
For the uploadPhoto
mutation, Apollo Server would return the selected file as Promise
, which contains a bunch of details about the file, such as: createReadStream
, filename
, mimetype
and encoding
. In this tutorial, we’ll only be making use of the first two, so we extract them from the object. Using createReadStream
, we stream the file directly to Cloudinary. Since it is an asynchronous operation we wrap it in a Promise
and await
s it. If the Promise
was resolved, that is, the file was uploaded successfully to Cloudinary, we create a new object containing the uploaded file name and the Cloudinary path to the file. Then we push the new object to the photos array and finally return the new object.
Lastly, if there was an error uploading the file to Cloudinary, we can console log the error.
Before we wrap up the GraphQL API, let’s quickly add our environment variables. Create a .env
file directly in the server
directory and add the code below in it:
// server/.env
CLOUD_NAME=YOUR_CLOUD_NAME
API_KEY=YOUR_API_KEY
API_SECRET=YOUR_API_SECRET
Remember to replace the placeholders with your actual account details.
Finally, let’s start the server:
$ node src/index.js
The server should be running on [http://localhost:4000](http://localhost:4000)
As already mentioned, the frontend app will be built with Vue.js, so let’s create a new Vue.js app using the Vue CLI:
$ vue create client
When prompted, press enter to select the default preset. Start the app and leave it running while we build on it:
$ cd client
$ yarn serve
The app should be running on http://localhost:8080
Once the Vue app is created, let’s install the necessary dependencies:
$ npm install vue-apollo graphql-tag graphql apollo-cache-inmemory apollo-client apollo-upload-client
Out of these dependencies, the one that is new and that I’d like to point out is [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client)
. It’s a package for Apollo Client that allows us to send GraphQL multipart requests. It will be used in place of apollo-link
.
Next, let’s configure Vue Apollo and these dependencies. Update main.js
as below:
// client/src/main.js
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloClient } from 'apollo-client'
import { createUploadLink } from 'apollo-upload-client'
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import App from './App.vue'
Vue.config.productionTip = false
Vue.use(VueApollo)
const apolloClient = new ApolloClient({
link: createUploadLink({ uri: 'http://localhost:4000' }),
cache: new InMemoryCache()
})
const apolloProvider = new VueApollo({
defaultClient: apolloClient
})
new Vue({
apolloProvider,
render: h => h(App)
}).$mount('#app')
You’ll notice here we are using createUploadLink
from apollo-upload-client
to create the ApolloClient
link, passing to it our GraphQL API endpoint.
To give our app a bit of styling, we’ll be pulling in UIKit. Add the line below to head
section of index.html
:
<!-- client/public/index.html -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.1.5/css/uikit.min.css" />
We’ll start with the GraphQL query for fetching all our photo. Create a graphql
directory inside the client/src
directory and with in, create a AllPhotos.js
file and paste the code below into it:
// client/src/graphql/AllPhotos.js
import gql from 'graphql-tag'
export default gql`query allPhotos {
allPhotos {
filename
path
}
}`
For the learning purposes of this tutorial, we’ll be making use of just the App.vue
component. So let’s update it as below:
// client/src/App.vue
<template>
<section class="uk-section">
<div class="uk-container uk-container-small">
<h2>Photo Album</h2>
<div class="uk-grid uk-child-width-1-3@m">
<div class="uk-margin" v-for="(photo, index) in allPhotos" :key="index">
<div class="uk-card uk-card-default">
<div class="uk-card-media-top">
<img :src="photo.path">
</div>
<div class="uk-card-body">{{ photo.filename }}</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
import ALL_PHOTOS from "./graphql/AllPhotos";
export default {
name: "app",
apollo: {
allPhotos: ALL_PHOTOS
}
};
</script>
Within the apollo
object, we add the ALL_PHOTOS
query to fetch all photos and save it in a allPhotos
. Once allPhotos
is populated with data from our GraphQL API, we display the photos by looping through them.
If we view our app, we should get something similar to below:
Of course, we need to have uploaded some photos before we can see them. Let’s implement that now. Still inside the graphql
directory, create a UploadPhoto.js
and paste the code below into it:
// client/src/graphql/UploadPhoto.js
import gql from 'graphql-tag'
export default gql`mutation uploadPhoto($photo: Upload!) {
uploadPhoto(photo: $photo) {
filename
path
}
}`
Next, add the snippet below to the template
section of App.vue
, just below the Photo Album heading:
// client/src/App.vue
<div class="uk-margin">
<input type="file" accept="image/*" @change="uploadPhoto">
</div>
Here, we have a file input field that accepts only images. On change of the input field a uploadPhoto
method is triggered.
In the script
section, add:
// client/src/App.vue
import UPLOAD_PHOTO from "./graphql/UploadPhoto";
methods: {
async uploadPhoto({ target }) {
await this.$apollo.mutate({
mutation: UPLOAD_PHOTO,
variables: {
photo: target.files[0]
},
update: (store, { data: { uploadPhoto } }) => {
const data = store.readQuery({ query: ALL_PHOTOS });
data.allPhotos.push(uploadPhoto);
store.writeQuery({ query: ALL_PHOTOS, data });
}
});
}
}
We get extract the target
from the input event, then call the mutate
method, passing to it the UPLOAD_PHOTO
mutation as well as the required argument (through the variables
object). We get the selected file from the files
on the target
object. Once the mutation is executed, we update the cache by adding the newly uploaded photo to the allPhotos
array.
So in this tutorial, we have seen how to handle file uploads in GraphQL using Apollo Server on the server side and Vue and Vue Apollo on the client side. Though we used Cloudinary to store our photos, you can also wrap it for any other cloud storage service or you can even save directly to your own local filesystem.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!