GraphQL Upload for TypeScript
A minimalistic, type-safe middleware for handling GraphQL file uploads in Node.js
Installation • Quick Start • Complete Examples • API • Contributing
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
- Bun Ready - Verified with Bun for install, test, and build workflows
- 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
- Quick Start
- Complete Examples
- API Documentation
- Security & Validation
- Architecture
- Migration Guide
- Contributing
- License
Installation
npm install graphql-upload-ts graphql
# or
yarn add graphql-upload-ts graphql
# or
pnpm add graphql-upload-ts graphql
# or
bun add graphql-upload-ts graphql
Requirements
- Node.js >= 16 or Bun >= 1.3.13
- 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
Click to expand example
When building schemas manually using GraphQL.js (without schema-first approach), you need to use the GraphQLUpload scalar directly:
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
Click to expand example
Complete setup with Apollo Server v4 and Express:
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
Click to expand example
Complete Koa setup with Apollo Server:
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
Click to expand example
Setup with GraphQL Yoga for a modern GraphQL server:
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
Click to expand example
For NestJS applications, you need special configuration:
// 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
Click to expand example
For TypeGraphQL, you need to create a custom scalar wrapper:
// 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
# 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)
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
Click to expand example
Complete example with image validation, resizing, and cloud storage:
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 maxFileSizeTOO_MANY_FILES- Too many files uploadedINVALID_FILE_TYPE- File type not allowedSTREAM_ERROR- Error reading file streamFIELD_SIZE_EXCEEDED- Non-file field too largeMISSING_MULTIPART_BOUNDARY- Invalid request formatINVALID_MULTIPART_REQUEST- Malformed multipart request
Architecture
The library uses a streaming architecture for efficient file handling:
- Request Parsing -
busboyparses multipart requests - File Buffering - Files are buffered to filesystem using
fs-capacitor - Promise Resolution - Upload promises resolve with file details
- Stream Creation - Resolvers can create multiple read streams from buffered files
- 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
- Middleware Order: Always apply the upload middleware BEFORE your GraphQL middleware
- File Processing: Process uploads inside resolvers, not after response
- Stream Handling: Always consume or destroy streams to prevent memory leaks
- Error Handling: Implement proper error handling for failed uploads
- NestJS: Use
overrideSendResponse: falseoption
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
# or
bun install
# Run the Jest suite with coverage
npm test
# Run the Bun suite
bun run test:bun
# Run tests with HTML 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 coverage-focused Node.js test runs
- Bun test - For native Bun runtime verification
- 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.