Handling File Uploads: Implementing Mutations in GraphQL
Learn how to handle file uploads in GraphQL using multipart/form-data. We'll guide you through setting up resolvers to capture and save files to your server.

Previously in this course, we explored handling mutation errors to ensure our API remains robust when things go wrong. Today, we add a feature essential for many real-world applications: accepting binary files via multipart/form-data.
While GraphQL is traditionally known for JSON-based payloads, file uploads are a frequent requirement. Because GraphQL operates over HTTP, we can leverage the standard multipart/form-data specification to send binary content alongside our GraphQL operations.
Understanding Multipart Requests
When a client sends a file, they aren't just sending a raw byte stream; they are wrapping that file in a "multipart" container. This format allows the browser to package form fields (like a user's name) and binary files (like an avatar image) into a single request.
In a standard GraphQL request, your Content-Type is application/json. For file uploads, the client switches this to multipart/form-data. Your server needs to know how to parse this specific format, extract the file stream, and handle the asynchronous nature of writing that data to a disk or cloud storage.
Implementing File Uploads in Your Schema

To support file uploads, we introduce the Upload scalar. Many GraphQL servers, including Apollo Server, support this out of the box via community packages.
First, update your SDL to define a mutation that accepts a file:
GraphQLscalar Upload type Mutation { uploadFile(file: Upload!): Boolean }
By adding the Upload scalar, you instruct your GraphQL engine to look for the file stream within the multipart request.
Saving Files from Resolvers
Once the server receives the multipart/form-data request, your resolver receives a promise that resolves to an object containing the file's metadata (filename, mimetype, encoding) and a createReadStream function.
Here is how you implement the resolver to save that file to a local uploads/ directory:
JAVASCRIPTimport fs from CE9178">'fs'; import path from CE9178">'path'; const resolvers = { Mutation: { uploadFile: async (_, { file }) => { const { createReadStream, filename } = await file; const stream = createReadStream(); const pathName = path.join(__dirname, CE9178">'uploads', filename); return new Promise((resolve, reject) => { stream .pipe(fs.createWriteStream(pathName)) .on(CE9178">'finish', () => resolve(true)) .on(CE9178">'error', (err) => reject(false)); }); }, }, };
Why Streams?
We use createReadStream() and .pipe() because files can be large. If we tried to load the entire file into memory before saving it, we would quickly crash our server under load. Streaming allows us to pipe data from the incoming request directly to the disk, keeping memory usage constant regardless of file size. For those building more robust architectures, consider learning about handling large file uploads: streaming to S3 and async processing to offload this work from your primary server.
Hands-on Exercise
- Install the
graphql-upload-minimalpackage to add theUploadscalar support to your Apollo server. - Create an
uploads/folder in your project root. - Update your
typeDefsto include theUploadscalar and auploadFilemutation. - Implement the resolver shown above and test it using a tool like Postman, setting the request type to
form-datawith a key offile.
Common Pitfalls
- Forgetting to define the scalar: The
Uploadscalar isn't built into the core GraphQL spec. You must define it in yourtypeDefsand map it to the implementation provided by your server library. - Blocking the event loop: Always use streams. Never use
fs.readFileSyncfor uploads, as it will block all other users from interacting with your server until the file is fully processed. - Unsafe Filenames: Never trust the
filenameprovided by the client. It could contain malicious paths like../../etc/passwd. Always sanitize the filename by stripping directory separators before saving it to disk.
Frequently Asked Questions
Does GraphQL handle multiple files at once?
Yes, you can define your mutation argument as [Upload!]! to accept a list of files.
Can I upload files directly to S3?
Yes. Instead of piping to fs.createWriteStream, you would pipe the stream into an S3 upload utility from the AWS SDK.
Is multipart/form-data the only way to upload files? No. You could also upload the file to a temporary store (like S3) using a signed URL and then send the resulting file URL to your GraphQL mutation. This is often preferred for very large files.
Recap
Handling file uploads in GraphQL requires shifting from application/json to multipart/form-data. By using the Upload scalar and streaming the binary data to your storage destination, you keep your server performant and memory-efficient.
Up next: We will dive into real-time updates by exploring Subscriptions and the Pub/Sub model.
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs โ the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

Custom Email & File Storage System on Cloudflare (Google Workspace Alternative)
Your own private email + file storage suite on your domain โ unlimited mailboxes, no per-seat fees. A self-owned Google Workspace alternative for a flat ~$5/month.


