Back to Blog
Lesson 51 of the GraphQL: Your First GraphQL Schema & Server course
API ArchitectureSeptember 7, 20264 min read

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.

GraphQLNode.jsFile UploadsAPI DevelopmentBackend
From below of monitor of modern computer with opened files on blue screen

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

From below of monitor of modern computer with opened files on blue screen

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:

GraphQL
scalar 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:

JAVASCRIPT
import 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

  1. Install the graphql-upload-minimal package to add the Upload scalar support to your Apollo server.
  2. Create an uploads/ folder in your project root.
  3. Update your typeDefs to include the Upload scalar and a uploadFile mutation.
  4. Implement the resolver shown above and test it using a tool like Postman, setting the request type to form-data with a key of file.

Common Pitfalls

  • Forgetting to define the scalar: The Upload scalar isn't built into the core GraphQL spec. You must define it in your typeDefs and map it to the implementation provided by your server library.
  • Blocking the event loop: Always use streams. Never use fs.readFileSync for uploads, as it will block all other users from interacting with your server until the file is fully processed.
  • Unsafe Filenames: Never trust the filename provided 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.

Similar Posts