Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

graphql-upload-nextjs

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

graphql-upload-nextjs

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.

latest
Source
npmnpm
Version
2.0.3
Version published
Weekly downloads
35
52.17%
Maintainers
1
Weekly downloads
 
Created
Source

graphql-upload-nextjs

npm version CI license GraphQL multipart request spec Node.js TypeScript

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.

Features

  • Implements the GraphQL multipart request spec.
  • Designed for Next.js App Router route handlers.
  • Supports single file uploads, multiple file uploads, and operation batching.
  • File deduplication (one file mapped to multiple operation paths).
  • MIME type verified via magic bytes using file-type, not trusting client headers.
  • Unknown binary files fall back to application/octet-stream instead of the client-provided MIME type.
  • Configurable allowedTypes, maxFileSize, and maxFiles.
  • Only reads 4KB for MIME detection — streams the rest directly from Blob.
  • Spec-compatible property names: filename, mimetype, encoding, createReadStream.

Installation

npm install graphql-upload-nextjs @apollo/server @as-integrations/next graphql next

Migrating from graphql-upload

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.

Usage

Schema and Resolvers

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.

Route Handler

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;

Configuration

await uploadProcess(request, context, server, {
    allowedTypes: ["image/jpeg", "image/png", "text/plain"],
    maxFileSize: 10 * 1024 * 1024, // 10MB
    maxFiles: 10,
});
OptionTypeDefaultDescription
allowedTypesstring[]undefinedRestrict MIME types. Checked against the detected MIME type; unknown binary files are application/octet-stream.
maxFileSizenumberundefinedMax file size in bytes. Must be finite and non-negative. Returns 413 if exceeded.
maxFilesnumberundefinedMax files per request. Must be finite and non-negative. Returns 413 if exceeded.

Client Usage

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 } });
  }} />;
}

API Reference

Core Exports

ExportTypeDescription
GraphQLUploadGraphQLScalarTypeThe Upload scalar for your GraphQL schema.
uploadProcessfunctionProcesses a multipart request with file uploads.
UploadclassHolds a promise that resolves with file upload details.

File Object

The resolved file object passed to resolvers:

PropertyTypeDescription
createReadStream() => ReadableStreamCreates a Node.js readable stream of the file contents. Replayable.
encodingstringTransfer encoding (always 'binary' with FormData API).
filenamestringOriginal file name from the client.
fileSizenumberFile size in bytes.
mimetypestringMIME type verified via magic bytes.

Utility Exports

ExportTypeDescription
bufferToStream(buffer: Buffer) => ReadableStreamConverts 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) => objectParses JSON and validates it's a non-null, non-array object.
setValueAtPath(obj, path, value) => voidSets 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 | nullValidates map entries are arrays of string paths. Returns error or null.

TypeScript Interfaces

ExportDescription
FileResolved file object with filename, mimetype, encoding, fileSize, createReadStream.
FileStreamFormData file entry with a stream() method.
FormDataFileRaw file entry from multipart FormData.
MinimalRequestRequest interface compatible with Next.js and Web API.

Spec Compliance

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.

Supported Capabilities

Spec CapabilityStatusDetails
operations field (JSON-encoded GraphQL operation)SupportedParsed and validated as object or array.
map field (file-to-path mapping)SupportedValidated: each entry must be an array of string paths.
Single file uploadSupportedFile mapped via "variables.file" path.
File list uploadSupportedFiles mapped via "variables.files.0", "variables.files.1", etc.
BatchingSupportedOperations as array, paths prefixed with operation index ("0.variables.file").
File deduplicationSupportedOne file field mapped to multiple operation paths.
object-path dot-notationSupportedHandles nested objects and array indices.
File upload streams in resolversSupportedcreateReadStream() returns a Node.js readable stream.
Missing file → rejected promiseSupportedUpload promise rejected with "File missing in the request."
maxFiles / maxFileSize limitsSupportedReturns HTTP 413 when exceeded.

Additions Beyond the Spec

FeatureDescription
MIME magic byte verificationReal file type detected via file-type, not trusting client-provided Content-Type.
allowedTypes filteringServer-side restriction of accepted MIME types.
Unknown binary fallbackFiles with no recognized magic bytes and non-text contents are reported as application/octet-stream.
fileSize propertyFile size in bytes available on the resolved file object.

Limitations

These are inherent trade-offs of using the Web FormData API for Next.js App Router compatibility. They do not affect normal spec usage.

LimitationReason
No field ordering validationThe spec requires operationsmap → 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 maxFieldSizeThe 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 abortThe 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 streamingNext.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.

Example

A full example project is available at examples/example-graphql-upload-nextjs/.

Contributing

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.

Security

Please read SECURITY.md before reporting security-sensitive issues.

License

MIT. See LICENSE.

Acknowledgements

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.

Keywords

Apollo Server

FAQs

Package last updated on 10 Jun 2026

Did you know?

Socket

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.

Install

Related posts