GraphQL and file upload using React and Node.js
Recently I was implementing file upload feature using GraphQL. After exploring more I realized that now we have following two approach to implement it.
- Use of
apollo-upload-server
withexpress
orkoa
orhappi
or other App server as middleware - Use of
apollo-upload-server
withapollo-server
Though both are pretty much similar but little different. This post is going to discuss these two approaches for Server side GraphQL implementation for file upload.
Basics of file upload
HTML input
with type file
is used to upload single or multiple files to server. File upload form uses multipart/form-data
encoding type and send payload in boundary format to send the file data to server. File upload process has following parts
input
file type HTML control. This allow user to select file from local machine.- HTML
form
with encoding type asmultipart/form-data
which uses selected files and create payload with multiple parts as below
POST https://localhost:8000/api/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=xxBOUNDARYxx
Content-Length: {POST body length in bytes}
--xxBOUNDARYxx
Content-Disposition: form-data; name="profileImage"; filename="my_new_profile_image.png"
Content-Type: image/png
{image file data bytes}
--xxBOUNDARYxx--
- Server API to handle
multipart/form-data
and either write file to local disk or perform some business logic as per need.
In this post, I am going to discuss how to use React.js
, Node.js
and Apollo GraphQL
to upload file.
GraphQL File Upload API for Server Side
GraphQL multipart request specification
multipart request for GraphQL has been documented very well in https://github.com/jaydenseric/graphql-multipart-request-spec . I am going to summarize it in brief in below.
GraphQL multipart request payload is divided into following three boundaries
operations
: A JSON encoded operations object with files replaced with null
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="operations"{ "query": "mutation ($file: Upload!) { uploadSingleFile(file: $file) { id } }", "variables": { "file": null } }
map
: A JSON encoded map of where files occurred in the operations. For each file, the key is the file multipart form fieldname
and thevalue
is an array of operations paths.
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="map"
{ "0": ["variables.file"] }
File fields
: Each file extracted from the operations object with a unique, arbitrary field name.
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="0"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--------------------------cec8e8123c05ba25--
GraphQL Multipart Example
GraphQL Operation:
{
query: `
mutation($files: [Upload!]!) {
multipleUpload(files: $files) {
id
}
}
`,
variables: {
files: [
File, // b.txt
File // c.txt
]
}
}
Using cURL
request:
curl localhost:3001/graphql \
-F operations='{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }' \
-F map='{ "0": ["variables.files.0"], "1": ["variables.files.1"] }' \
-F 0=@b.txt \
-F 1=@c.txt
Request Payload:
--------------------------ec62457de6331cad
Content-Disposition: form-data; name="operations"
{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }
--------------------------ec62457de6331cad
Content-Disposition: form-data; name="map"
{ "0": ["variables.files.0"], "1": ["variables.files.1"] }
--------------------------ec62457de6331cad
Content-Disposition: form-data; name="0"; filename="b.txt"
Content-Type: text/plain
Bravo file content.
--------------------------ec62457de6331cad
Content-Disposition: form-data; name="1"; filename="c.txt"
Content-Type: text/plain
Charlie file content.
--------------------------ec62457de6331cad--
Server Side GraphQL implementation for File upload as middleware for App Server
Note
: This post will use single file upload as reference to discuss the concepts.
Following are components to support file upload at GraphQL API.
GraphQLUpload
as schema type forUpload
which is type forfile
# index.graphql
scalar Uploadtype File {
id: ID!
path: String!
filename: String!
mimetype: String!
encoding: String!
}type Mutation {
uploadSingleFile(file: Upload!): File!
}// index.js for resolverconst { GraphQLUpload } = require('apollo-upload-server');const resolvers = {
Upload: GraphQLUpload,
Mutation: {
uploadSingleFile: async (root, { file }) => { const { stream, mimetype } = await file; // Now use stream to either write file at local disk or CDN
}
}
};
- Add middleware to
App
server. Following is example to add forKoa
server
const { apolloUploadKoa } = require('apollo-upload-server');const app = new Koa();
const router = new KoaRouter();// Middleware to support file upload for graphql
app.use(apolloUploadKoa());
Server Side GraphQL implementation for File upload with apollo-server
File upload is not by default supported for express
or koa
or other node.js
app server. However this feature is by default supported with apollo-server
. Therefore following are not required with apollo-server
- No need to declare
Upload
scaler in schema - No need to declare
Upload
in resolver - No need to define
middleware
in app server
Following are details needed to support file upload with apollo-server
- Define operation in schema
# index.graphqltype File {
id: ID!
path: String!
filename: String!
mimetype: String!
encoding: String!
}type Mutation {
uploadSingleFile(file: Upload!): File!
}
- Define resolver
const resolvers = {
Mutation: {
uploadSingleFile: async (root, { file }) => {const { stream, mimetype } = await file;// Now use stream to either write file at local disk or CDN
}
}
};
GraphQL file upload for React Client
apollo-upload-client
module is used for React
component to upload file using GraphQL
API. Following are two parts to use file upload for client side.
ApolloProvider
setup withapollo-upload-client
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from 'react-apollo';
import { HttpLink, InMemoryCache, ApolloClient } from 'apollo-client-preset';
import { ApolloLink } from 'apollo-link';
import { createUploadLink } from 'apollo-upload-client';
const uploadLink = createUploadLink({ uri: 'http://localhost:4000/graphql' });
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });const cache = new InMemoryCache();
// apollo client setup
const client = new ApolloClient({
cache,
link: ApolloLink.from([uploadLink, link])
})ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root'),
)
- Use of
Mutation
withinput
file
to call GraphQL API
import React from 'react';
import PropTypes from 'prop-types';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag'const UPLOAD_FILE = gql`
mutation($file: Upload!) {
uploadSingleFile(file: $file) {
path
}
}`;
const handleChange = async ( event, mutation ) => {
const {
target: {
validity,
files: [file],
}
} = event;
if (validity.valid) {
// Call graphql API
const { data: { uploadSingleFile } } = await mutation({
mutation: UPLOAD_FILE,
variables: { file },
fetchPolicy: 'no-cache',
});
// Use uploadSingleFile response
}
};
const UploadFile = ({ onChange, ...rest }) => {
return (
<Mutation mutation={UPLOAD_FILE} fetchPolicy="no-cache">
{ (mutation, { loading }) => (
<input
type="file"
required
onChange={event => handleChange(event, mutation)} />
/>
) }
</Mutation>
);
};