graphql-upload for Node.js GraphQL file uploads
Middleware and a scalar Upload to add support for GraphQL multipart requests (file uploads via queries and mutations) to various Node.js GraphQL servers.
At a glance
- What is it?
- graphql-upload adds GraphQL multipart request support to Node.js servers through Express or Koa middleware, an Upload scalar, and filesystem-backed buffering. It suits teams integrating file variables, but its Node.js, TypeScript, disk, and stream-management requirements require attention.
- Who is it for?
- Adopt graphql-upload when a Node.js GraphQL service needs multipart file uploads and can satisfy its runtime, temporary-disk, and stream-cleanup requirements. Do not choose it when a server-specific implementation is already available, when an older Node.js release is mandatory, or when uploads cannot be awaited safely.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 21 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The multipart upload gap graphql-upload fills
A normal GraphQL operation carries its inputs as variables. When one of those inputs is a file, the client needs the GraphQL multipart request format so an HTTP request can carry operation metadata and file parts together. graphql-upload is the server-side building block for that case. It is intended for Node.js applications that accept files through GraphQL queries or mutations, including services built with Express or Koa.
The package addresses more than schema validation. A normal GraphQL scalar receives a value from the request, but an uploaded file arrives as a stream. Resolver code needs a promise-like value that eventually exposes that stream, and it must retain enough control to await consumption, observe failure, and release temporary resources. graphql-upload supplies that contract through its Upload scalar and exports middleware for HTTP integration.
This is a low-level integration package rather than a complete upload service. The README and repository layout do not show destination-specific storage, database records, authorization policy, virus scanning, or upload progress reporting as features of this package. Those concerns remain application responsibilities. Its users are backend engineers wiring uploads into an existing GraphQL server, not teams looking for a hosted file-management product.
How graphql-upload bridges multipart parts and GraphQL variables
The data path starts when a client sends a request that follows the GraphQL multipart request specification. The multipart stream is parsed by busboy. The package first reads the operations and map fields because those describe which uploaded file belongs to which GraphQL variable and whether one file can be reused by multiple variables.
Once those fields are available, graphql-upload populates Upload scalar values in the GraphQL operations with promises. The parsed operations then continue through the server middleware chain toward GraphQL execution. A resolver can await the relevant Upload value, inspect the upload details, and call createReadStream to obtain a stream for storage or another processing task.
Buffering is provided by fs-capacitor. File content begins moving to temporary storage as soon as it arrives. The associated promise resolves without waiting for every byte to be written, allowing resolver execution to begin while the remaining file is still being received. Independent read streams can then be created as resolvers execute, which matters because asynchronous resolvers do not necessarily consume files in multipart receipt order.
The buffer is removed after all streams end or close and the server has responded. Remaining files are also cleaned when the process exits. This design explains both the benefit and the operational cost. graphql-upload avoids holding every upload only in memory and permits flexible resolver scheduling, but it makes temporary disk access, concurrency capacity, and lifecycle management part of the service configuration.
Installing graphql-upload and preparing its first integration
The README recommends checking whether an existing GraphQL multipart request server implementation is more suitable before assembling the middleware manually. For a direct npm installation, the documented command installs graphql-upload together with its graphql peer dependency:
npm install graphql-upload graphqlThe package version shown in package.json is 18.0.1. Its supported runtimes are Node.js ^22.13.0, ^24.0.0, or >=26.0.0. Applications outside that range are not listed as supported, so runtime compatibility should be checked before writing an Express or Koa integration.
Next, choose the module that matches the server. graphqlUploadKoa or graphqlUploadExpress must be placed immediately before the GraphQL middleware. A custom HTTP integration can instead use processRequest. The package has no main index module, so code must import one of the specific ECMAScript module paths exposed by the exports field. Relevant paths include graphqlUploadExpress.mjs and graphqlUploadKoa.mjs.
A schema built with separate SDL and resolvers also needs the Upload scalar wired into that schema. The scalar can then appear as the type of a query or mutation argument. The README points to GraphQLUpload.mjs for resolver usage rather than providing a complete application example in its installation section, so the actual schema and resolver code must be assembled against the server and schema tools already used by the application.
Resolver stream handling that a first upload must get right
The first useful resolver has a stricter lifecycle than many file-upload examples suggest. It must await the Upload promise and consume the resulting stream before the server sends its response. The README warns that responding early can disconnect the client while upload work is still underway.
createReadStream must also be called before the resolver returns. Calling it later, including from an unawaited asynchronous function or callback, throws an error. This restriction does not prevent an already-created stream from continuing after a response has been sent, but the documentation identifies few valid reasons for consuming such a stream after that point.
For several uploads, Promise.all waits for every operation and rejects as soon as one rejects. Promise.allSettled preserves each outcome independently, which is useful when processing several files asynchronously and the application needs to report failures per file. Neither option removes the need to handle individual stream errors.
When an upload is incomplete and will not be consumed, stream.destroy() should be used. Without that step, temporary resources may remain until later cleanup. Network interruptions and clients disconnecting early are named failure sources, so an integration that only handles successful multipart requests is incomplete. Storage handlers need their own rejection paths in addition to the GraphQL scalar conversion.
Disk pressure and temporary files in graphql-upload
The filesystem buffer makes large uploads less dependent on memory, but it does not make upload handling free. The process must have both read and write permission for the directory returned by os.tmpdir(). That directory therefore needs to be available under the account used by the Node.js service, including deployment environments that mount or isolate temporary storage.
Disk capacity must match the number and size of concurrent uploads the service permits. graphql-upload does not replace admission control, so an endpoint exposed without request limits could allow concurrent traffic to fill the temporary filesystem. The README calls out sufficient disk space for buffering the expected number of concurrent upload requests, but it does not document a package-specific disk quota or concurrency limit that can be assumed safe by default.
Cleanup depends on the full stream lifecycle. Files can be removed once every read stream has ended or closed and the response has completed, while leftover buffers are cleared at process exit. Abrupt termination behavior should therefore be considered during deployment planning, especially where containers may stop before graceful stream completion. This is an operational boundary of the filesystem-backed design, not a storage durability guarantee.
Release scope, license, and server alternatives
The repository is not archived, and its last push was on 2026-10-02. Recent releases include v18.0.1 on 2026-10-02, v18.0.0 on 2026-08-03, and v17.1.0 on 2026-07-21. These dates describe the recorded maintenance activity. They do not establish compatibility with every Node.js deployment, framework, or upload volume.
package.json identifies the project as MIT licensed and marks the package as sideEffects false. The exported package files are limited to the upload scalar, middleware modules, request processing helpers, and stream utilities. That structure permits focused server integration, but it also means there is no default index export for a conventional package-root import.
A server-specific GraphQL multipart request implementation can be a real alternative. The README explicitly tells integrators to check known implementations first because one may fit the environment better than a manual setup. The practical difference is integration ownership: graphql-upload exposes busboy parsing, fs-capacitor buffering, Upload values, and Express or Koa middleware, leaving the application to wire it before its GraphQL handler. A server implementation may embed upload behavior in a wider server configuration, although this project does not document the internal mechanisms of those alternatives, so no stronger comparison can be made from the repository and README.
TypeScript users also incur configuration work. The documented settings are compilerOptions.allowJs set to true, compilerOptions.maxNodeModuleJsDepth set to a reasonably large value such as 10, and compilerOptions.module set to "node16" or "nodenext". These requirements come from consuming types shipped with the package's ECMAScript modules.
Editorial conclusion
Adopt graphql-upload when a Node.js GraphQL service needs multipart file uploads and can satisfy its runtime, temporary-disk, and stream-cleanup requirements. Do not choose it when a server-specific implementation is already available, when an older Node.js release is mandatory, or when uploads cannot be awaited safely. Verify the selected server integration, Upload scalar wiring, temporary storage capacity, rejection handling, and cleanup behavior before accepting production uploads.
Frequently asked questions
Which Node.js versions does graphql-upload support?
The package supports Node.js ^22.13.0, ^24.0.0, and >=26.0.0.
How is graphql-upload installed and wired into a server?
Install it with `npm install graphql-upload graphql`. Use `graphqlUploadKoa` or `graphqlUploadExpress` immediately before the GraphQL middleware, or use `processRequest` for custom middleware.
Where does graphql-upload buffer uploaded files?
It uses fs-capacitor to buffer file contents on the filesystem. The process needs read and write access to the directory identified by `os.tmpdir()`.
What must a resolver do with an Upload value?
The resolver should await the Upload promise and call `createReadStream` before the resolver returns. It must also handle promise rejection and stream errors, and await stream completion before the server responds.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jaydenseric-graphql-upload)Was this useful?