
Company News
Andrew Becherer Joins Socket as Chief Information Security Officer
Socket’s first CISO brings deep experience securing high-growth SaaS companies as open source supply chain threats accelerate.
graphql-upload-nextjs
Advanced tools
The middleware and Upload scalar in this package enable GraphQL multipart requests (file uploads via queries and mutations) in your Apollo Server Next.js integration.
A GraphQL multipart request spec implementation for Next.js App Router with Apollo Server. Enables file uploads via GraphQL mutations using the Upload scalar, with built-in MIME type verification via magic bytes.
application/octet-stream instead of the client-provided MIME type.allowedTypes, maxFileSize, and maxFiles.Blob.filename, mimetype, encoding, createReadStream.npm install graphql-upload-nextjs @apollo/server @as-integrations/next graphql next
graphql-upload uses Express/Koa middleware that is incompatible with Next.js App Router route handlers. This package provides the same Upload scalar and file object interface for the Next.js environment.
Property names match the original: filename, mimetype, encoding, createReadStream. An additional fileSize property is also available.
Before (graphql-upload with Express):
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'
app.use(graphqlUploadExpress())
After (graphql-upload-nextjs with App Router):
import { GraphQLUpload, uploadProcess } from 'graphql-upload-nextjs'
// In your route handler:
if (request.headers.get("content-type")?.includes("multipart/form-data")) {
return await uploadProcess(request, context, server);
}
Resolver code stays the same — filename, mimetype, encoding, and createReadStream work identically.
import path from 'node:path'
import { createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream'
import { gql } from '@apollo/client' // Optional: syntax highlighting only.
import { ApolloServer } from '@apollo/server'
import { startServerAndCreateNextHandler } from '@as-integrations/next'
import { type File, GraphQLUpload, uploadProcess } from 'graphql-upload-nextjs'
import type { NextRequest } from 'next/server.js'
const typeDefs = gql`
scalar Upload
type File {
encoding: String!
filename: String!
fileSize: Int!
mimetype: String!
uri: String!
}
type Query {
default: Boolean!
}
type Mutation {
uploadFile(file: Upload!): File!
uploadFiles(files: [Upload!]!): [File!]!
}
`
interface FileResponse {
encoding: string;
filename: string;
fileSize: number;
mimetype: string;
uri: string;
}
const resolvers = {
Mutation: {
uploadFile: async (
_parent: undefined,
{ file }: { file: Promise<File> },
): Promise<FileResponse> => {
const { createReadStream, encoding, filename, fileSize, mimetype } = await file;
const safeName = path.basename(filename);
return new Promise((resolve, reject) => {
pipeline(
createReadStream(),
createWriteStream(`./uploads/${safeName}`),
(error) => {
if (error) reject(new Error("Error during file upload."));
else resolve({ encoding, filename: safeName, fileSize, mimetype, uri: `/${safeName}` });
},
);
});
},
uploadFiles: async (
_parent: undefined,
{ files }: { files: Promise<File>[] },
): Promise<FileResponse[]> => {
const resolvedFiles = await Promise.all(files);
return Promise.all(resolvedFiles.map(async ({ createReadStream, encoding, filename, fileSize, mimetype }) => {
const safeName = path.basename(filename);
return new Promise<FileResponse>((resolve, reject) => {
pipeline(
createReadStream(),
createWriteStream(`./uploads/${safeName}`),
(error) => {
if (error) reject(new Error(`Error during upload of ${safeName}.`));
else resolve({ encoding, filename: safeName, fileSize, mimetype, uri: `/${safeName}` });
},
);
});
}));
},
},
Query: { default: async () => true },
Upload: GraphQLUpload,
}
Security: Always sanitize filenames with
path.basename()to prevent path traversal attacks. Never write uploaded files to publicly accessible directories in production.
const server = new ApolloServer({ resolvers, typeDefs });
const handler = startServerAndCreateNextHandler<NextRequest>(server, {
context: async (req: NextRequest) => ({
ip: req.headers.get("x-forwarded-for") || "",
req,
}),
});
const requestHandler = async (request: NextRequest) => {
if (request.headers.get("content-type")?.includes("multipart/form-data")) {
const context = { ip: request.headers.get("x-forwarded-for") || "", req: request };
return await uploadProcess(request, context, server);
}
return handler(request);
};
export const GET = requestHandler;
export const POST = requestHandler;
export const OPTIONS = requestHandler;
await uploadProcess(request, context, server, {
allowedTypes: ["image/jpeg", "image/png", "text/plain"],
maxFileSize: 10 * 1024 * 1024, // 10MB
maxFiles: 10,
});
| Option | Type | Default | Description |
|---|---|---|---|
allowedTypes | string[] | undefined | Restrict MIME types. Checked against the detected MIME type; unknown binary files are application/octet-stream. |
maxFileSize | number | undefined | Max file size in bytes. Must be finite and non-negative. Returns 413 if exceeded. |
maxFiles | number | undefined | Max files per request. Must be finite and non-negative. Returns 413 if exceeded. |
When using a GraphQL client library (apollo-upload-client, urql, extract-files), file uploads are constructed automatically per the spec.
import { gql, useMutation } from '@apollo/client';
const UPLOAD = gql`
mutation UploadFile($file: Upload!) {
uploadFile(file: $file) { filename mimetype fileSize }
}
`;
function Uploader() {
const [upload] = useMutation(UPLOAD);
return <input type="file" onChange={(e) => {
const file = e.target.files?.[0];
if (file) upload({ variables: { file } });
}} />;
}
| Export | Type | Description |
|---|---|---|
GraphQLUpload | GraphQLScalarType | The Upload scalar for your GraphQL schema. |
uploadProcess | function | Processes a multipart request with file uploads. |
Upload | class | Holds a promise that resolves with file upload details. |
The resolved file object passed to resolvers:
| Property | Type | Description |
|---|---|---|
createReadStream | () => ReadableStream | Creates a Node.js readable stream of the file contents. Replayable. |
encoding | string | Transfer encoding (always 'binary' with FormData API). |
filename | string | Original file name from the client. |
fileSize | number | File size in bytes. |
mimetype | string | MIME type verified via magic bytes. |
| Export | Type | Description |
|---|---|---|
bufferToStream | (buffer: Buffer) => ReadableStream | Converts a Buffer to a Node.js readable stream. |
parseOperationsJSON | (input: string) => object | object[] | Parses the operations field (object or array for batching). |
sanitizeAndValidateJSON | (input: string) => object | Parses JSON and validates it's a non-null, non-array object. |
setValueAtPath | (obj, path, value) => void | Sets a value at a dot-notation path in a nested object. |
streamToBuffer | (stream: ReadableStream) => Promise<Buffer> | Collects a readable stream into a Buffer. |
validateMap | (map: object) => string | null | Validates map entries are arrays of string paths. Returns error or null. |
| Export | Description |
|---|---|
File | Resolved file object with filename, mimetype, encoding, fileSize, createReadStream. |
FileStream | FormData file entry with a stream() method. |
FormDataFile | Raw file entry from multipart FormData. |
MinimalRequest | Request interface compatible with Next.js and Web API. |
Implements the GraphQL multipart request specification by jaydenseric.
The spec defines a multipart form field structure with three ordered fields (operations, map, file fields) and enables nesting files anywhere within operations, operation batching, file deduplication, and file upload streams in resolvers.
| Spec Capability | Status | Details |
|---|---|---|
operations field (JSON-encoded GraphQL operation) | Supported | Parsed and validated as object or array. |
map field (file-to-path mapping) | Supported | Validated: each entry must be an array of string paths. |
| Single file upload | Supported | File mapped via "variables.file" path. |
| File list upload | Supported | Files mapped via "variables.files.0", "variables.files.1", etc. |
| Batching | Supported | Operations as array, paths prefixed with operation index ("0.variables.file"). |
| File deduplication | Supported | One file field mapped to multiple operation paths. |
object-path dot-notation | Supported | Handles nested objects and array indices. |
| File upload streams in resolvers | Supported | createReadStream() returns a Node.js readable stream. |
| Missing file → rejected promise | Supported | Upload promise rejected with "File missing in the request." |
maxFiles / maxFileSize limits | Supported | Returns HTTP 413 when exceeded. |
| Feature | Description |
|---|---|
| MIME magic byte verification | Real file type detected via file-type, not trusting client-provided Content-Type. |
allowedTypes filtering | Server-side restriction of accepted MIME types. |
| Unknown binary fallback | Files with no recognized magic bytes and non-text contents are reported as application/octet-stream. |
fileSize property | File size in bytes available on the resolved file object. |
These are inherent trade-offs of using the Web FormData API for Next.js App Router compatibility. They do not affect normal spec usage.
| Limitation | Reason |
|---|---|
| No field ordering validation | The spec requires operations → map → files ordering. The Web FormData API retrieves fields by name (formData.get('operations')), not by position, so ordering cannot be validated. In practice, all major client libraries (apollo-upload-client, extract-files) send fields in the correct order. |
No maxFieldSize | The original graphql-upload limits non-file field sizes via busboy. Next.js parses the full request body into FormData before our code runs, so field size limiting is not possible at this layer. |
encoding is always 'binary' | FormData does not expose the transfer encoding of file parts. The original graphql-upload reads this from busboy's stream events. In practice, transfer encoding is rarely used by resolvers. |
| No mid-stream abort | The original uses fs-capacitor to buffer uploads to disk, allowing resolvers to abort an in-progress upload. With FormData, the entire request body is already parsed by the runtime. However, resolvers can still choose not to call createReadStream() to skip processing. |
Blob.stream() instead of busboy streaming | Next.js route handlers receive a Request object (Web API), not a Node.js IncomingMessage. There is no access to the raw request stream. Blob.stream() provides a replayable readable stream per file. |
A full example project is available at examples/example-graphql-upload-nextjs/.
Contributions are welcome! To get started:
git clone https://github.com/lafittemehdy/graphql-upload-nextjs.git
cd graphql-upload-nextjs
npm install
npm run build
npm test
CI runs automatically on push and pull requests via GitHub Actions (build + test on Node.js 22/24, example build + lint).
Please read CONTRIBUTING.md and CODE_OF_CONDUCT.md before opening a pull request.
Please read SECURITY.md before reporting security-sensitive issues.
MIT. See LICENSE.
Sincere gratitude to jaydenseric for the GraphQL multipart request specification and graphql-upload, and to meabed for graphql-upload-ts which served as a valuable reference.
Finally, heartfelt gratitude to my mom for her unwavering support, which has allowed me to dedicate my time to working on open-source software.
FAQs
The middleware and Upload scalar in this package enable GraphQL multipart requests (file uploads via queries and mutations) in your Apollo Server Next.js integration.
The npm package graphql-upload-nextjs receives a total of 35 weekly downloads. As such, graphql-upload-nextjs popularity was classified as not popular.
We found that graphql-upload-nextjs demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Socket’s first CISO brings deep experience securing high-growth SaaS companies as open source supply chain threats accelerate.

Company News
Replit is integrating Socket Firewall into its AI-powered development experience to help protect builders from malicious open source packages.

Security News
npm confirmed a tooling bug incorrectly marked several one-character packages as security holders and said it was working on a rollback.