File Uploads in GraphQL

Intermediate
11 min

File Uploads in GraphQL

GraphQL requests are JSON, and JSON has no binary type. Uploading a file therefore needs one of three strategies: a multipart request that the server understands, a server framework with built-in file support, or keeping files out of GraphQL entirely with signed URLs. After this lesson you will be able to implement each and choose the right one.

The Multipart Request Specification

The community "GraphQL multipart request" specification defines how to send an operation plus files in a single multipart/form-data request. The graphql-upload package implements it for Node servers and provides the Upload scalar:

bash
npm install graphql-upload
javascript
import { GraphQLUpload, graphqlUploadExpress } from "graphql-upload"; const typeDefs = `#graphql scalar Upload type Mutation { uploadAvatar(file: Upload!): User! } `; const resolvers = { Upload: GraphQLUpload, Mutation: { uploadAvatar: async (_p, { file }, { user, storage, db }) => { const { filename, mimetype, createReadStream } = await file; if (!mimetype.startsWith("image/")) throw new GraphQLError("Images only"); const key = `avatars/${user.id}-${Date.now()}-${filename}`; await storage.put(key, createReadStream()); return db.users.update(user.id, { avatarKey: key }); }, }, }; app.use("/graphql", graphqlUploadExpress({ maxFileSize: 5_000_000, maxFiles: 3 }), expressMiddleware(server));

The argument resolves to a promise of an object with createReadStream(), so the file is streamed rather than buffered in memory. Two details matter with Apollo Server: the upload middleware must run before expressMiddleware, and because multipart requests are a CSRF risk, Apollo's default csrfPrevention requires clients to send the header Apollo-Require-Preflight: true. On the client, createUploadLink from apollo-upload-client replaces HttpLink and sends File objects from a form automatically.

Built-In Support in GraphQL Yoga

GraphQL Yoga implements the same specification without extra packages and exposes files as standard File objects:

javascript
const yoga = createYoga({ schema: createSchema({ typeDefs: `scalar File type Mutation { uploadAvatar(file: File!): Boolean! }`, resolvers: { Mutation: { uploadAvatar: async (_p, { file }) => { const bytes = await file.arrayBuffer(); await storage.put(file.name, Buffer.from(bytes)); return true; }, }, }, }), });

file.name, file.type, file.size, file.stream() and file.arrayBuffer() are the same properties available in browsers.

Signed URLs: Keeping Files Out of GraphQL

Large files through the API server cost bandwidth, memory and request time. The pattern most production teams use is to let GraphQL hand out permission and let the client upload straight to object storage:

graphql
type Mutation { createUploadUrl(filename: String!, contentType: String!): UploadTicket! attachAvatar(key: String!): User! } type UploadTicket { url: String!, key: String! }

The client calls createUploadUrl, sends the file with a plain PUT to the returned pre-signed URL (S3, Cloudflare R2 and Google Cloud Storage support these), then calls attachAvatar with the key. GraphQL never touches the bytes, the API stays JSON-only, and storage handles size limits and virus scanning hooks.

| Approach | Best for | Trade-off | |---|---|---| | graphql-upload | Small files, existing Apollo and Express stack | Extra middleware, CSRF header, files pass through the API | | Yoga File scalar | Small files on Yoga | Files still pass through the API | | Signed URLs | Production, large files, mobile | Two round trips, storage-specific setup |

Common Mistakes

  • Reading the whole upload into memory with await file followed by buffering; stream to storage instead.
  • Forgetting maxFileSize and maxFiles limits, which lets a single client exhaust the server.
  • Trusting the client-supplied mimetype; check magic bytes server-side when it matters.
Quick Quiz
Question 1 of 2

Why does Apollo Server reject multipart uploads unless a special header is present?

Key Takeaways

  • JSON cannot carry binary data, so uploads need the multipart specification or a separate channel.
  • graphql-upload adds a streamable Upload scalar; register its middleware before Apollo's and send Apollo-Require-Preflight: true.
  • GraphQL Yoga supports uploads natively through a File scalar with the standard browser File API.
  • Signed URLs keep large files out of the API server and are the preferred production approach.
  • Always enforce size, count and content-type limits on the server.

Next lesson: Consuming GraphQL from a Frontend — send operations from a browser with fetch and understand what a client library adds.

File Uploads in GraphQL - GraphQL | CodeYourCraft | CodeYourCraft