包详细信息

graphql-upload-ts

meabed153.5kMIT2.1.3

TypeScript-first middleware and Upload scalar for GraphQL multipart requests (file uploads) with support for Apollo Server, Express, Koa, and more.

graphql, graphql-upload, graphql-upload-typescript, file-upload

自述文件

GraphQL Upload for TypeScript

NPM version Build Status Test Coverage Downloads License TypeScript Node A minimalistic, type-safe middleware for handling GraphQL file uploads in Node.js InstallationQuick StartComplete ExamplesAPIContributing

✨ Features

  • 🚀 Full TypeScript Support - Written in TypeScript with complete type definitions
  • 📦 Framework Agnostic - Works with Express, Koa, Apollo Server, and more
  • 🔒 Type-Safe - Strict TypeScript mode enabled with comprehensive type coverage
  • 🎯 Production Ready - Battle-tested with 91%+ test coverage
  • High Performance - Efficient file streaming with configurable limits
  • 🛡️ Security First - Built-in file validation and sanitization
  • 📝 Well Documented - Extensive documentation and real-world examples
  • 🔄 Dual Module Support - CommonJS and ESM modules included

📋 Table of Contents

📦 Installation

npm install graphql-upload-ts graphql
# or
yarn add graphql-upload-ts graphql
# or
pnpm add graphql-upload-ts graphql

Requirements

  • Node.js >= 16
  • GraphQL >= 0.13.1

Build System

This package uses Rollup for bundling and provides CommonJS builds for maximum compatibility. The build configuration has been optimized for simplicity and reliability.

🚀 Quick Start

Basic Setup with Express

import express from 'express';
import { graphqlHTTP } from 'express-graphql';
import { graphqlUploadExpress, GraphQLUpload } from 'graphql-upload-ts';
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      hello: {
        type: GraphQLString,
        resolve: () => 'Hello World',
      },
    },
  }),
  mutation: new GraphQLObjectType({
    name: 'Mutation',
    fields: {
      uploadFile: {
        type: GraphQLString,
        args: {
          file: { type: GraphQLUpload },
        },
        async resolve(_, { file }) {
          const { filename, createReadStream } = await file;
          const stream = createReadStream();
          // Process your file here
          return `File ${filename} uploaded successfully`;
        },
      },
    },
  }),
});

const app = express();

// Important: graphqlUploadExpress middleware must come BEFORE graphqlHTTP
app.use(
  '/graphql',
  graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }),
  graphqlHTTP({ schema, graphiql: true })
);

app.listen(4000, () => {
  console.log('Server running on http://localhost:4000/graphql');
});

📚 Complete Examples

Manual Schema Construction with GraphQL.js

<summary>Click to expand example</summary> When building schemas manually using GraphQL.js (without schema-first approach), you need to use the GraphQLUpload scalar directly: typescript import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLNonNull, GraphQLList, } from 'graphql'; import { GraphQLUpload } from 'graphql-upload-ts'; import fs from 'fs'; import path from 'path'; // Define custom types const FileType = new GraphQLObjectType({ name: 'File', fields: { filename: { type: GraphQLString }, mimetype: { type: GraphQLString }, encoding: { type: GraphQLString }, url: { type: GraphQLString }, }, }); // Create schema with mutations const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { hello: { type: GraphQLString, resolve: () => 'Hello World', }, }, }), mutation: new GraphQLObjectType({ name: 'Mutation', fields: { // Single file upload singleUpload: { type: FileType, args: { file: { type: new GraphQLNonNull(GraphQLUpload), }, }, async resolve(_, { file }) { const { filename, mimetype, encoding, createReadStream } = await file; // Create upload directory if it doesn't exist const uploadDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } // Save file to filesystem const filePath = path.join(uploadDir, filename); const stream = createReadStream(); const writeStream = fs.createWriteStream(filePath); stream.pipe(writeStream); await new Promise((resolve, reject) => { writeStream.on('finish', resolve); writeStream.on('error', reject); }); return { filename, mimetype, encoding, url: `/uploads/${filename}`, }; }, }, // Multiple file uploads multipleUpload: { type: new GraphQLList(FileType), args: { files: { type: new GraphQLNonNull( new GraphQLList(new GraphQLNonNull(GraphQLUpload)) ), }, }, async resolve(_, { files }) { const uploadedFiles = []; for (const file of files) { const { filename, mimetype, encoding, createReadStream } = await file; // Process each file... uploadedFiles.push({ filename, mimetype, encoding }); } return uploadedFiles; }, }, }, }), });

Express + Apollo Server v4

<summary>Click to expand example</summary> Complete setup with Apollo Server v4 and Express: typescript import express from 'express'; import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; import { graphqlUploadExpress, GraphQLUpload } from 'graphql-upload-ts'; import { createServer } from 'http'; import cors from 'cors'; import bodyParser from 'body-parser'; // Type definitions const typeDefs = `#graphql scalar Upload type File { filename: String! mimetype: String! encoding: String! url: String! } type Query { hello: String } type Mutation { singleUpload(file: Upload!): File! multipleUpload(files: [Upload!]!): [File!]! } `; // Resolvers const resolvers = { Upload: GraphQLUpload, Query: { hello: () => 'Hello world!', }, Mutation: { singleUpload: async (parent, { file }) => { const { createReadStream, filename, mimetype, encoding } = await file; // Stream file to cloud storage, filesystem, etc. const stream = createReadStream(); // Example: Save to filesystem const path = require('path'); const fs = require('fs'); const out = fs.createWriteStream(path.join(__dirname, 'uploads', filename)); stream.pipe(out); await new Promise((resolve, reject) => { out.on('finish', resolve); out.on('error', reject); }); return { filename, mimetype, encoding, url: `/uploads/${filename}`, }; }, multipleUpload: async (parent, { files }) => { const uploadedFiles = []; for (const file of files) { const { createReadStream, filename, mimetype, encoding } = await file; // Process each file uploadedFiles.push({ filename, mimetype, encoding, url: `/uploads/${filename}`, }); } return uploadedFiles; }, }, }; // Server setup async function startServer() { const app = express(); const httpServer = createServer(app); const server = new ApolloServer({ typeDefs, resolvers, plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], }); await server.start(); // Apply upload middleware BEFORE Apollo Server app.use( '/graphql', cors(), bodyParser.json(), graphqlUploadExpress({ maxFileSize: 10 * 1024 * 1024, // 10 MB maxFiles: 5, }), expressMiddleware(server, { context: async ({ req }) => ({ token: req.headers.token }), }) ); await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve)); console.log('🚀 Server ready at http://localhost:4000/graphql'); } startServer();

Koa + Apollo Server

<summary>Click to expand example</summary> Complete Koa setup with Apollo Server: typescript import Koa from 'koa'; import Router from '@koa/router'; import { ApolloServer } from '@apollo/server'; import { koaMiddleware } from '@as-integrations/koa'; import { graphqlUploadKoa, GraphQLUpload } from 'graphql-upload-ts'; import { createServer } from 'http'; const typeDefs = `#graphql scalar Upload type File { filename: String! mimetype: String! encoding: String! } type Query { hello: String } type Mutation { uploadFile(file: Upload!): File! uploadFiles(files: [Upload!]!): [File!]! } `; const resolvers = { Upload: GraphQLUpload, Query: { hello: () => 'Hello from Koa!', }, Mutation: { uploadFile: async (_, { file }) => { const { filename, mimetype, encoding, createReadStream } = await file; // Process file stream const stream = createReadStream(); // Example: Upload to S3 // const { S3 } = require('@aws-sdk/client-s3'); // const { Upload } = require('@aws-sdk/lib-storage'); // const s3 = new S3({ region: 'us-east-1' }); // // const upload = new Upload({ // client: s3, // params: { // Bucket: 'my-bucket', // Key: filename, // Body: stream, // ContentType: mimetype, // }, // }); // // await upload.done(); return { filename, mimetype, encoding }; }, uploadFiles: async (_, { files }) => { const uploadPromises = files.map(async (file) => { const { filename, mimetype, encoding, createReadStream } = await file; // Process each file return { filename, mimetype, encoding }; }); return Promise.all(uploadPromises); }, }, }; async function startServer() { const app = new Koa(); const router = new Router(); const httpServer = createServer(app.callback()); const server = new ApolloServer({ typeDefs, resolvers, }); await server.start(); // Apply upload middleware app.use(graphqlUploadKoa({ maxFileSize: 10 * 1024 * 1024, // 10 MB maxFiles: 5, })); // Apply Apollo Server middleware router.all( '/graphql', koaMiddleware(server, { context: async ({ ctx }) => ({ token: ctx.headers.token }), }) ); app.use(router.routes()); app.use(router.allowedMethods()); httpServer.listen(4000, () => { console.log('🚀 Server ready at http://localhost:4000/graphql'); }); } startServer();

Express + GraphQL Yoga

<summary>Click to expand example</summary> Setup with GraphQL Yoga for a modern GraphQL server: typescript import express from 'express'; import { createYoga, createSchema } from 'graphql-yoga'; import { graphqlUploadExpress, GraphQLUpload } from 'graphql-upload-ts'; const schema = createSchema({ typeDefs: /* GraphQL */ ` scalar Upload type File { filename: String! mimetype: String! encoding: String! content: String! } type Query { hello: String } type Mutation { readTextFile(file: Upload!): File! uploadImage(file: Upload!): File! uploadDocuments(files: [Upload!]!): [File!]! } `, resolvers: { Upload: GraphQLUpload, Query: { hello: () => 'Hello from Yoga!', }, Mutation: { readTextFile: async (_, { file }) => { const { filename, mimetype, encoding, createReadStream } = await file; // Read text file content const stream = createReadStream(); const chunks = []; for await (const chunk of stream) { chunks.push(chunk); } const content = Buffer.concat(chunks).toString('utf-8'); return { filename, mimetype, encoding, content, }; }, uploadImage: async (_, { file }) => { const { filename, mimetype, encoding, createReadStream } = await file; // Validate image if (!mimetype.startsWith('image/')) { throw new Error('File must be an image'); } const stream = createReadStream(); // Example: Process with sharp for image manipulation // const sharp = require('sharp'); // const processedImage = await sharp(stream) // .resize(800, 600) // .jpeg({ quality: 80 }) // .toBuffer(); return { filename, mimetype, encoding, content: 'Image processed successfully', }; }, uploadDocuments: async (_, { files }) => { const results = []; for (const file of files) { const { filename, mimetype, encoding } = await file; results.push({ filename, mimetype, encoding, content: `Document ${filename} uploaded`, }); } return results; }, }, }, }); const app = express(); // Apply upload middleware BEFORE yoga app.use(graphqlUploadExpress({ maxFileSize: 10 * 1024 * 1024, // 10 MB maxFiles: 10, })); // Create and use Yoga const yoga = createYoga({ schema, graphiql: { title: 'GraphQL Yoga with File Uploads', }, }); app.use('/graphql', yoga); app.listen(4000, () => { console.log('🧘 Server is running on http://localhost:4000/graphql'); });

NestJS Integration

<summary>Click to expand example</summary> For NestJS applications, you need special configuration: typescript // app.module.ts import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { graphqlUploadExpress } from 'graphql-upload-ts'; @Module({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: true, // Disable built-in upload handling uploads: false, }), ], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply( graphqlUploadExpress({ maxFileSize: 10 * 1024 * 1024, // 10 MB maxFiles: 5, // Important for NestJS! overrideSendResponse: false, }) ) .forRoutes('graphql'); } } // upload.resolver.ts import { Resolver, Mutation, Args } from '@nestjs/graphql'; import { GraphQLUpload, FileUpload } from 'graphql-upload-ts'; import { createWriteStream } from 'fs'; @Resolver() export class UploadResolver { @Mutation(() => Boolean) async uploadFile( @Args({ name: 'file', type: () => GraphQLUpload }) file: Promise<FileUpload>, ): Promise<boolean> { const { createReadStream, filename } = await file; return new Promise((resolve, reject) => { createReadStream() .pipe(createWriteStream(`./uploads/${filename}`)) .on('finish', () => resolve(true)) .on('error', reject); }); } }

TypeGraphQL Integration

<summary>Click to expand example</summary> For TypeGraphQL, you need to create a custom scalar wrapper: typescript // upload.scalar.ts import { GraphQLUpload } from 'graphql-upload-ts'; import { Scalar, CustomScalar } from 'type-graphql'; import { GraphQLScalarType, GraphQLError } from 'graphql'; // Create a custom Upload scalar for TypeGraphQL @Scalar('Upload') export class UploadScalar implements CustomScalar<any, any> { description = 'The `Upload` scalar type represents a file upload.'; parseValue(value: any) { return GraphQLUpload.parseValue(value); } serialize(value: any) { return GraphQLUpload.serialize(value); } parseLiteral(ast: any) { return GraphQLUpload.parseLiteral(ast, null); } } // Define the FileUpload type for TypeScript import { Stream } from 'stream'; import { Field, ObjectType, InputType } from 'type-graphql'; interface Upload { filename: string; mimetype: string; encoding: string; createReadStream: () => Stream; } // Output type for file information @ObjectType() export class FileInfo { @Field() filename: string; @Field() mimetype: string; @Field() encoding: string; @Field() url: string; } // Input type for mutations with files and additional fields @InputType() export class CreatePostInput { @Field() title: string; @Field() content: string; @Field(() => [String], { nullable: true }) tags?: string[]; // Note: File upload fields are handled separately in resolver args } // resolver.ts import { Resolver, Mutation, Arg, Query } from 'type-graphql'; import { GraphQLUpload } from 'graphql-upload-ts'; import { FileInfo, CreatePostInput } from './types'; import { createWriteStream } from 'fs'; import path from 'path'; @Resolver() export class PostResolver { // Simple file upload @Mutation(() => FileInfo) async uploadFile( @Arg('file', () => GraphQLUpload) file: Promise<Upload> ): Promise<FileInfo> { const { filename, mimetype, encoding, createReadStream } = await file; // Save file to disk const savePath = path.join(__dirname, 'uploads', filename); const stream = createReadStream(); const writeStream = createWriteStream(savePath); stream.pipe(writeStream); await new Promise((resolve, reject) => { writeStream.on('finish', resolve); writeStream.on('error', reject); }); return { filename, mimetype, encoding, url: `/uploads/${filename}`, }; } // File upload with additional form fields @Mutation(() => Boolean) async createPostWithImage( @Arg('data') data: CreatePostInput, @Arg('image', () => GraphQLUpload) image: Promise<Upload>, @Arg('thumbnail', () => GraphQLUpload, { nullable: true }) thumbnail?: Promise<Upload> ): Promise<boolean> { // Process the main image const mainImage = await image; const { filename, createReadStream } = mainImage; // Save the main image const imagePath = path.join(__dirname, 'uploads', 'posts', filename); const imageStream = createReadStream(); const imageWriteStream = createWriteStream(imagePath); imageStream.pipe(imageWriteStream); // Process thumbnail if provided if (thumbnail) { const thumbFile = await thumbnail; const thumbPath = path.join(__dirname, 'uploads', 'posts', 'thumbnails', thumbFile.filename); const thumbStream = thumbFile.createReadStream(); const thumbWriteStream = createWriteStream(thumbPath); thumbStream.pipe(thumbWriteStream); } // Save post data to database console.log('Creating post with:', { title: data.title, content: data.content, tags: data.tags, imagePath, }); // In a real app, save to database here return true; } // Multiple file uploads with metadata @Mutation(() => [FileInfo]) async uploadMultipleFiles( @Arg('files', () => [GraphQLUpload]) files: Promise<Upload>[], @Arg('descriptions', () => [String], { nullable: true }) descriptions?: string[] ): Promise<FileInfo[]> { const uploadedFiles: FileInfo[] = []; for (let i = 0; i < files.length; i++) { const file = await files[i]; const { filename, mimetype, encoding, createReadStream } = file; const description = descriptions?.[i] || ''; // Save each file const savePath = path.join(__dirname, 'uploads', filename); const stream = createReadStream(); const writeStream = createWriteStream(savePath); stream.pipe(writeStream); await new Promise((resolve, reject) => { writeStream.on('finish', resolve); writeStream.on('error', reject); }); // Store metadata if needed console.log(`File ${filename} uploaded with description: ${description}`); uploadedFiles.push({ filename, mimetype, encoding, url: `/uploads/${filename}`, }); } return uploadedFiles; } } // server.ts - Setting up the server import 'reflect-metadata'; import express from 'express'; import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import { buildSchema } from 'type-graphql'; import { graphqlUploadExpress } from 'graphql-upload-ts'; import { PostResolver } from './resolver'; import { UploadScalar } from './upload.scalar'; async function bootstrap() { // Build TypeGraphQL schema const schema = await buildSchema({ resolvers: [PostResolver], scalarsMap: [{ type: Object, scalar: UploadScalar }], }); // Create Apollo Server const server = new ApolloServer({ schema }); await server.start(); // Create Express app const app = express(); // IMPORTANT: Apply upload middleware BEFORE Apollo Server app.use( '/graphql', graphqlUploadExpress({ maxFileSize: 10 * 1024 * 1024, // 10 MB maxFiles: 10, }), express.json(), expressMiddleware(server) ); app.listen(4000, () => { console.log('Server is running on http://localhost:4000/graphql'); }); } bootstrap(); #### Example GraphQL Mutations graphql # Simple file upload mutation UploadFile($file: Upload!) { uploadFile(file: $file) { filename mimetype url } } # Upload with additional fields mutation CreatePost($data: CreatePostInput!, $image: Upload!, $thumbnail: Upload) { createPostWithImage(data: $data, image: $image, thumbnail: $thumbnail) } # Multiple files with descriptions mutation UploadMultiple($files: [Upload!]!, $descriptions: [String!]) { uploadMultipleFiles(files: $files, descriptions: $descriptions) { filename url } } #### Client-Side Example (using Apollo Client) javascript import { gql, useMutation } from '@apollo/client'; const UPLOAD_WITH_DATA = gql` mutation CreatePost($data: CreatePostInput!, $image: Upload!) { createPostWithImage(data: $data, image: $image) } `; function PostForm() { const [createPost] = useMutation(UPLOAD_WITH_DATA); const handleSubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const file = formData.get('image'); const data = { title: formData.get('title'), content: formData.get('content'), tags: formData.get('tags').split(','), }; await createPost({ variables: { data, image: file, }, }); }; return ( <form onSubmit={handleSubmit}> <input name="title" placeholder="Post Title" required /> <textarea name="content" placeholder="Content" required /> <input name="tags" placeholder="Tags (comma-separated)" /> <input name="image" type="file" required /> <button type="submit">Create Post</button> </form> ); }

Image Upload with Validation

<summary>Click to expand example</summary> Complete example with image validation, resizing, and cloud storage: typescript import express from 'express'; import { graphqlHTTP } from 'express-graphql'; import { graphqlUploadExpress, GraphQLUpload } from 'graphql-upload-ts'; import { validateMimeType, validateFileExtension, sanitizeFilename } from 'graphql-upload-ts'; import sharp from 'sharp'; import { S3 } from '@aws-sdk/client-s3'; import { Upload } from '@aws-sdk/lib-storage'; import crypto from 'crypto'; const schema = buildSchema(` scalar Upload type Image { id: String! originalName: String! filename: String! mimetype: String! size: Int! width: Int! height: Int! url: String! thumbnailUrl: String! } type Mutation { uploadProfileImage(file: Upload!): Image! uploadGalleryImages(files: [Upload!]!): [Image!]! } type Query { hello: String } `); const s3 = new S3({ region: process.env.AWS_REGION }); const resolvers = { Upload: GraphQLUpload, Mutation: { uploadProfileImage: async (_, { file }) => { const { filename, mimetype, createReadStream } = await file; // Validate image type const mimeValidation = validateMimeType(mimetype, [ 'image/jpeg', 'image/png', 'image/webp', ]); if (!mimeValidation.isValid) { throw new Error(mimeValidation.error); } // Validate file extension const extValidation = validateFileExtension(filename, [ '.jpg', '.jpeg', '.png', '.webp', ]); if (!extValidation.isValid) { throw new Error(extValidation.error); } // Sanitize filename const sanitized = sanitizeFilename(filename); const uniqueFilename = `${Date.now()}-${crypto.randomBytes(8).toString('hex')}-${sanitized}`; // Read the stream into a buffer for processing const stream = createReadStream(); const chunks = []; for await (const chunk of stream) { chunks.push(chunk); } const buffer = Buffer.concat(chunks); // Process image with sharp const image = sharp(buffer); const metadata = await image.metadata(); // Validate image dimensions if (metadata.width < 100 || metadata.height < 100) { throw new Error('Image must be at least 100x100 pixels'); } // Create main image (max 1920x1080) const mainImage = await image .resize(1920, 1080, { fit: 'inside', withoutEnlargement: true, }) .jpeg({ quality: 85, progressive: true }) .toBuffer(); // Create thumbnail (200x200) const thumbnail = await sharp(buffer) .resize(200, 200, { fit: 'cover', position: 'center', }) .jpeg({ quality: 80 }) .toBuffer(); // Upload main image to S3 const mainUpload = new Upload({ client: s3, params: { Bucket: process.env.S3_BUCKET, Key: `images/${uniqueFilename}`, Body: mainImage, ContentType: 'image/jpeg', CacheControl: 'max-age=31536000', }, }); // Upload thumbnail to S3 const thumbUpload = new Upload({ client: s3, params: { Bucket: process.env.S3_BUCKET, Key: `thumbnails/${uniqueFilename}`, Body: thumbnail, ContentType: 'image/jpeg', CacheControl: 'max-age=31536000', }, }); const [mainResult, thumbResult] = await Promise.all([ mainUpload.done(), thumbUpload.done(), ]); return { id: crypto.randomUUID(), originalName: filename, filename: uniqueFilename, mimetype: 'image/jpeg', size: mainImage.length, width: metadata.width, height: metadata.height, url: mainResult.Location, thumbnailUrl: thumbResult.Location, }; }, uploadGalleryImages: async (_, { files }) => { const uploadPromises = files.map(async (filePromise) => { const file = await filePromise; // Process each image similarly // ... implementation }); return Promise.all(uploadPromises); }, }, }; const app = express(); // Configure upload middleware with strict limits for images app.use( '/graphql', graphqlUploadExpress({ maxFileSize: 5 * 1024 * 1024, // 5 MB max for images maxFiles: 10, // Max 10 images at once }), graphqlHTTP({ schema, rootValue: resolvers, graphiql: true, }) ); app.listen(4000, () => { console.log('Image upload server running on http://localhost:4000/graphql'); });

📖 API Documentation

Middleware Functions

graphqlUploadExpress(options?)

Express middleware for handling multipart/form-data requests.

import { graphqlUploadExpress } from 'graphql-upload-ts';

app.use('/graphql', graphqlUploadExpress({
  maxFileSize: 10000000,  // 10 MB for file uploads (default: 5 MB)
  maxFiles: 10,           // Max number of files (default: Infinity)
  maxFieldSize: 1000000,  // 1 MB for JSON fields (default: 1 MB)
}));

graphqlUploadKoa(options?)

Koa middleware for handling multipart/form-data requests.

import { graphqlUploadKoa } from 'graphql-upload-ts';

app.use(graphqlUploadKoa({
  maxFileSize: 10000000, // 10 MB
  maxFiles: 10,
}));

Types

FileUpload

The promise returned from uploaded files contains:

interface FileUpload {
  filename: string;
  mimetype: string;
  encoding: string;
  fieldName: string;
  createReadStream: (options?: ReadStreamOptions) => NodeJS.ReadableStream;
}

interface ReadStreamOptions {
  encoding?: BufferEncoding;
  highWaterMark?: number;
}

UploadOptions

Configuration options for the middleware:

interface UploadOptions {
  maxFieldSize?: number;  // Max size of non-file fields like JSON (default: 1 MB)
  maxFileSize?: number;   // Max size per file upload (default: 5 MB) 
  maxFiles?: number;      // Max number of files (default: Infinity)
}

Scalar Type

GraphQLUpload

The GraphQL scalar type for file uploads. Use it in your schema:

import { GraphQLUpload } from 'graphql-upload-ts';

// For schema-first approach (SDL)
const resolvers = {
  Upload: GraphQLUpload,
  // ... other resolvers
};

// For code-first approach
import { GraphQLScalarType } from 'graphql';
const Upload: GraphQLScalarType = GraphQLUpload;

🛡️ Security & Validation

Built-in Protections

The library includes several security features:

  • File size limits - Prevent large file DoS attacks
  • File count limits - Restrict number of concurrent uploads
  • Field size limits - Limit non-file field sizes
  • Filename sanitization - Remove unsafe characters from filenames
  • MIME type validation - Optional MIME type restrictions

Validation Utilities

import { 
  validateMimeType, 
  validateFileExtension, 
  sanitizeFilename 
} from 'graphql-upload-ts';

// Validate MIME type
const mimeResult = validateMimeType(mimetype, ['image/jpeg', 'image/png']);
if (!mimeResult.isValid) {
  throw new Error(mimeResult.error);
}

// Validate file extension
const extResult = validateFileExtension(filename, ['.jpg', '.jpeg', '.png']);
if (!extResult.isValid) {
  throw new Error(extResult.error);
}

// Sanitize filename for safe storage
const safe = sanitizeFilename('../../dangerous/file name!.txt');
// Returns: "dangerous-file-name.txt"

Error Handling

The library provides custom error classes:

import { UploadError, UploadErrorCode } from 'graphql-upload-ts';

try {
  // Upload logic
} catch (error) {
  if (error instanceof UploadError) {
    switch (error.code) {
      case UploadErrorCode.FILE_TOO_LARGE:
        // Handle large file
        break;
      case UploadErrorCode.INVALID_FILE_TYPE:
        // Handle invalid type
        break;
      // ... handle other cases
    }
  }
}

Error codes available:

  • FILE_TOO_LARGE - File exceeds maxFileSize
  • TOO_MANY_FILES - Too many files uploaded
  • INVALID_FILE_TYPE - File type not allowed
  • STREAM_ERROR - Error reading file stream
  • FIELD_SIZE_EXCEEDED - Non-file field too large
  • MISSING_MULTIPART_BOUNDARY - Invalid request format
  • INVALID_MULTIPART_REQUEST - Malformed multipart request

🏗️ Architecture

The library uses a streaming architecture for efficient file handling:

  1. Request Parsing - busboy parses multipart requests
  2. File Buffering - Files are buffered to filesystem using fs-capacitor
  3. Promise Resolution - Upload promises resolve with file details
  4. Stream Creation - Resolvers can create multiple read streams from buffered files
  5. Cleanup - Temporary files are automatically cleaned up after response

This architecture allows:

  • Processing files in any order
  • Multiple reads of the same file
  • Backpressure handling
  • Automatic cleanup

🔄 Migration Guide

From graphql-upload v15+

This library is a TypeScript-first alternative with similar API:

// Before (graphql-upload)
const { graphqlUploadExpress } = require('graphql-upload');
const { GraphQLUpload } = require('graphql-upload');

// After (graphql-upload-ts)
import { graphqlUploadExpress, GraphQLUpload } from 'graphql-upload-ts';

Main differences:

  • Full TypeScript support with strict types
  • CommonJS build for maximum compatibility
  • Built-in validation utilities
  • Custom error classes
  • Modern Node.js features (16+)

Important Notes

  1. Middleware Order: Always apply the upload middleware BEFORE your GraphQL middleware
  2. File Processing: Process uploads inside resolvers, not after response
  3. Stream Handling: Always consume or destroy streams to prevent memory leaks
  4. Error Handling: Implement proper error handling for failed uploads
  5. NestJS: Use overrideSendResponse: false option

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Development

# Install dependencies
npm install

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Build the library (using Rollup)
npm run build

# Run linting (using Biome)
npm run lint

# Format code (using Biome)
npm run format

# Type checking
npm run typecheck

Build Configuration

The project uses:

  • Rollup - For bundling the TypeScript source into CommonJS format
  • Biome - For linting and formatting (replacing ESLint and Prettier)
  • Jest - For testing with comprehensive coverage
  • TypeScript - With strict mode enabled for type safety

📄 License

MIT © Mohamed Meabed

🙏 Acknowledgments

This library is a TypeScript fork of graphql-upload by Jayden Seric. The original library was exceptionally well designed, and this fork aims to maintain that quality while adding TypeScript support and modern features.

🔗 Links


Made with ❤️ by Mohamed Meabed

更新日志

Changelog

[Unreleased]

Changed

  • Replaced Rolldown with Rollup for more stable and reliable builds
  • Migrated build configuration to CommonJS format for better compatibility
  • Simplified TypeScript configuration and build process
  • Removed ESM builds in favor of CommonJS-only distribution
  • Updated all dependencies to latest versions
  • Improved package.json exports with comprehensive entry points
  • Made README examples collapsible for better readability
  • Converted internal file names to kebab-case for consistency

Fixed

  • Resolved ESM import errors by using CommonJS-only build
  • Fixed TypeScript import errors related to file casing
  • Improved build system stability and reliability

2.2.0 (2025-01-03)

Features

  • Add comprehensive error handling with custom error classes and error codes
  • Add file validation utilities (MIME type, file extension, filename sanitization)
  • Add dual module support (CommonJS and ESM)
  • Improve TypeScript support with strict mode enabled
  • Add extensive test coverage (91%+ coverage with 119 tests)

Improvements

  • Migrate from ESLint/Prettier to Biome for unified tooling
  • Update TypeScript target from ES5 to ES2020
  • Replace all any types with proper type inference
  • Use optional chaining (?.) instead of non-null assertions (!)
  • Improve Express middleware to properly wait for request completion
  • Update all dependencies to latest versions

Documentation

  • Complete README rewrite with comprehensive examples
  • Add manual schema construction examples with GraphQL.js
  • Add complete examples for Apollo Server v4, Koa, GraphQL Yoga, and NestJS
  • Add detailed image upload example with validation and S3 integration
  • Add security and validation documentation
  • Add migration guide from graphql-upload

Developer Experience

  • Remove Mocha, use Jest exclusively for testing
  • Add separate tsconfig files for different build targets
  • Improve build process for better performance
  • Add comprehensive type definitions

2.1.2 (2024-02-23)

  • Update package export to export fs-capacitor

2.1.1 (2024-02-23)

  • rework file upload stream handling to fix multiple file upload and filesize issues
  • Improve readme and add more information about using overrideSendResponse
  • Make overrideSendResponse default to false if processRequest is not provided
  • Added more examples in /examples folder
  • Update types and tests
  • Update packages

2.1.0 (2023-08-08)

  • Feat: add overrideSendResponse to optionally disable override send response in express - thank you (@Gherciu for the PR)
  • Update packages

2.0.9 (2023-07-19)

  • Update packages

2.0.5 (2022-12-25)

  • Fix release script
  • Update packages
  • Update package.json exports

2.0.2 (2022-07-24)

  • Fix release content

2.0.1 (2022-07-24)

  • Rewrite the codebase to typescript.
  • Initial release of the new version.