Handling File Uploads in GraphQL and Vue
Publikováno: 19.6.2019
In this tutorial, I'll be showing you how to hand file uploads in GraphQL by building a fullstack app. The tutorial will be divided into two main sections: building the GraphQL API and the frontend...
In this tutorial, I'll be showing you how to hand file uploads in GraphQL by building a fullstack app. The tutorial will be divided into two main sections: building the GraphQL API and 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.
Prerequisites
This tutorial assumes you conformatable with GraphQL and using it in a Vue app. Otherwise you might want to catch up with Super Simple GraphQL with Node and Build a Blog With Vue, GraphQL, and Apollo Client.
What we'll be building
For the purpose of this tutorial, we'll be building a simple 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:
Getting your Cloudinary keys
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.
Building the GraphQL server
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 simply 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 simply console log the erorr.
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)
Building the frontend app
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" />
Fetching all photos
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 sake of simplicity, 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:
Uploading photo
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.
Conclusion
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 easily wrap it for any other cloud storage service or you can even save directly to your own local filesystem.