npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

graphql-upload-nextjs

v2.0.0

Published

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.

Readme

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.
  • 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

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"],
    maxFiles: 10,
    maxFileSize: 10 * 1024 * 1024, // 10MB
});

| Option | Type | Default | Description | |---|---|---|---| | allowedTypes | string[] | undefined | Restrict MIME types. Verified via magic bytes. | | maxFiles | number | undefined | Max files per request. Returns 413 if exceeded. | | maxFileSize | number | undefined | Max file size in bytes. 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

| 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. |

File Object

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. |

Utility Exports

| 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. |

TypeScript Interfaces

| 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. |

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 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. |

Additions Beyond the Spec

| 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. | | fileSize property | File 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.

| Limitation | Reason | |---|---| | No field ordering validation | The 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 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. |

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).

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.