Uploading files to GraphQL Server with URQL
Even though I have used GraphQL for a little while, I have never stepped upon a
file upload requirement. But theres always a first time no? And mine
was the last week, when I had to upload a image file as part of a GraphQL mutation
.
This is the schema in question:
input BrandCreateInput {
name: String!
logo: Upload!
}
mutation ($input: BrandCreateInput!) {
brandCreate(input: $input) {
id
name
logo
}
}
The Upload
type above refers to Apollo’s GraphQL Upload
type.
The Upload
type represents a multipart/form-data
upload which is the one
used when uploading a file with the FormData
object with your browser.
For my project I’m using URQL, a GraphQL client by Formidable Labs which
brings bindings for React, Svelte, Vue and Vanilla JS. For the UI components
I’m using Svelte. For example purposes I will use the same stack. First we will
integrate URQL to our Svelte project, then we will define a service which will
be responsible of performing the mutation
using the URQL client. And finally
we will implement a Svelte component which will have a form with a name
text
input and a logo
file input. Values from this form are then provided to the
service to perform the mutation.
Setup URQL for your project
Let’s install a couple packages:
urql
: The core library for URQL@urql/svelte
: Svelte bindings for URQL@urql/exchange-multipart-fetch
: A URQL exchange to enable file uploads withmultipart/form-data
.
npm install urql @urql/svelte @urql/exchange-multipart-fetch
Then create a URQL client and provide the multipartFetchExchange
from the @urql/exchange-multipart-fetch
library.
I’m exporting the instance of this URQL client from the module scope as follows:
// lib/utils/urql.ts
import { multipartFetchExchange } from '@urql/exchange-multipart-fetch';
import { createClient, dedupExchange, cacheExchange } from 'urql';
export const urqlClient = createClient({
url: '/graphql',
exchanges: [dedupExchange, cacheExchange, multipartFetchExchange],
});
Defining a ProductService
to be responsible for performing the mutation
I like to keep my logic and view or use case disengaged. so I tend to define services which takes care of data manipulation and API consumption and then use these services in my views or use cases.
The ProductService
is responsible for performing product related operations,
for instance creating new brands.
// lib/services/product.ts
import { urqlClient } from '$lib/utils/urql';
import type { Brand } from '$graphql/schema';
const CreateBrandMutation = `
mutation ($input: BrandCreateInput!) {
brandCreate(input: $input) {
id
name
logo
}
}`;
const createBrand = async (name: string, logo: File): Promise<Brand> => {
const { data = {}, error } = await urqlClient
.mutation(CreateBrandMutation, {
input: {
name,
logo
}
})
.toPromise();
if (error) {
// Do actual error handling here
throw error;
}
return data.brandCreate;
}
export const productService = {
createBrand,
}
Retrieve the file from the user
With a URQL client in place, we are ready to actually ask the user for a file to upload.
To handle form logic I’m using manzana ⏤ I’m the author ⏤ which provides logic to handle form validation, values, errors and more with Svelte API primitives such as stores.
For example purposes, I will keep UI and logic as simple as possible.
<!-- CreateBrand.svelte -->
<script lang="ts">
import { newForm } from 'manzana';
import { productService } from '$lib/services/product';
const { handleSubmit, values, setFieldValue } = newForm<{
name: string;
logo: File;
}>({
initialValues: {
name: '',
logo: null
},
onSubmit: async (values) => {
await productService.createBrand(values.name, values.logo);
}
});
</script>
<form on:submit={handleSubmit}>
<input
type="text"
name="name"
placeholder="Apple Inc."
bind:value={$values.name}
/>
<input
type="file"
on:change={(event) => {
const files: FileList = event.target.files;
setFieldValue('logo', files[0]);
}}
/>
<button type="submit">
Create
</button>
</form>
Conclusion
Voilà! We have a file upload to a GraphQL server using Svelte and URQL! If you had any issues following up, please open a PR or issue here. I’m always happy to learn and help others!