GraphQL’s standard request format is JSON, so handling file uploads requires a special MIME type: multipart/form-data.

Let’s see it in action. Imagine a GraphQL mutation like this:

mutation UploadFile($file: Upload!) {
  uploadFile(file: $file) {
    id
    filename
    mimetype
    encoding
  }
}

When a client sends a file, the request body isn’t a simple JSON object anymore. Instead, it’s a multipart/form-data payload. This payload contains multiple "parts," each with its own Content-Type header. One of these parts will hold the actual file data, and another will contain the JSON-encoded GraphQL operation (the mutation and its variables).

Here’s a simplified (and conceptual) look at what that multipart/form-data request might look like on the wire:

----------------------------7e3b0a1c1a3f
Content-Disposition: form-data; name="operations"

{
  "query": "mutation UploadFile($file: Upload!) { uploadFile(file: $file) { id filename mimetype encoding } }",
  "variables": {
    "file": null
  }
}
----------------------------7e3b0a1c1a3f
Content-Disposition: form-data; name="map"

{
  "0": ["variables.file"]
}
----------------------------7e3b0a1c1a3f
Content-Disposition: form-data; name="0"; filename="my_document.pdf"
Content-Type: application/pdf

<binary content of my_document.pdf>
----------------------------7e3b0a1c1a3f--

Notice the operations part. This contains the GraphQL query string and the variables object. Crucially, the variables.file is null here. The map part is where the magic happens: it tells the GraphQL server how to map the file part (named 0 in this example) to the correct variable (variables.file) in the operations. The 0 part itself contains the actual file data, along with its original filename and Content-Type.

The server-side GraphQL implementation, often using libraries like graphql-upload (for Node.js), parses this multipart/form-data request. It uses the operations and map parts to reconstruct the GraphQL request and associate the uploaded file with the correct argument. The file is then typically streamed to its final destination (e.g., cloud storage, local disk) without necessarily loading the entire file into memory.

The Upload scalar type in GraphQL is a custom scalar that signals to the client and server that this argument is expected to be a file upload. When a client sees an Upload type in a mutation signature, it knows to construct the request using multipart/form-data and include the file data in the appropriate part.

The core problem this solves is bridging the gap between standard JSON-based GraphQL requests and the need to transfer binary file data. Without multipart/form-data and the Upload scalar, you’d be forced into cumbersome workarounds like base64 encoding files within JSON, which is inefficient and bloats request sizes.

The graphql-upload library, for instance, provides a Upload scalar implementation that handles parsing the multipart request. It exposes the uploaded file as a stream or buffer, allowing you to process it without loading the entire content into memory. This streaming capability is essential for handling large files efficiently.

When implementing this on the server, you’ll typically configure your HTTP server framework (like Express, Koa, etc.) to use a middleware that can parse multipart/form-data requests before they reach your GraphQL endpoint. This middleware then makes the parsed file data available to your GraphQL resolver.

The graphql-upload library also requires you to use its specific graphql function or a compatible setup that understands how to process the uploaded files based on the operations and map fields within the multipart request. It’s not just about parsing the form data; it’s about correctly integrating that parsed data into the GraphQL execution engine.

The most surprising thing about this mechanism is how the map field acts as a symbolic link, directly connecting a named part of the multipart request to a specific path within the JSON operations payload. It’s a clever way to decouple the file’s position in the multipart body from its intended destination in the GraphQL variables, allowing for more flexible client implementations.

The next challenge you’ll encounter is handling file validation and security, such as checking MIME types, file sizes, and sanitizing filenames.

Want structured learning?

Take the full Graphql-tools course →