graphql-upload-ts
Version:
TypeScript-first middleware and Upload scalar for GraphQL multipart requests (file uploads) with support for Apollo Server, Express, Koa, and more.
1,424 lines (1,148 loc) • 36.9 kB
Markdown
# GraphQL Upload for TypeScript
<div align="center">
[](https://npm.im/graphql-upload-ts)
[](https://github.com/meabed/graphql-upload-ts/actions)
[](https://github.com/meabed/graphql-upload-ts)
[](https://www.npmjs.com/package/graphql-upload-ts)
[](https://github.com/meabed/graphql-upload-ts/blob/master/LICENSE)
[](https://www.typescriptlang.org/)
[](https://nodejs.org)
**A minimalistic, type-safe middleware for handling GraphQL file uploads in Node.js**
[Installation](#-installation) • [Quick Start](#-quick-start) • [Complete Examples](#-complete-examples) • [API](#-api) • [Contributing](#-contributing)
</div>
## ✨ 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](#-installation)
- [Quick Start](#-quick-start)
- [Complete Examples](#-complete-examples)
- [Manual Schema Construction](#manual-schema-construction-with-graphqljs)
- [Express + Apollo Server](#express--apollo-server-v4)
- [Koa + Apollo Server](#koa--apollo-server)
- [Express + GraphQL Yoga](#express--graphql-yoga)
- [NestJS Integration](#nestjs-integration)
- [Image Upload with Validation](#image-upload-with-validation)
- [API Documentation](#-api-documentation)
- [Security & Validation](#-security--validation)
- [Architecture](#-architecture)
- [Migration Guide](#-migration-guide)
- [Contributing](#-contributing)
- [License](#-license)
## 📦 Installation
```bash
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
```typescript
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
<details>
<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;
},
},
},
}),
});
```
</details>
### Express + Apollo Server v4
<details>
<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();
```
</details>
### Koa + Apollo Server
<details>
<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();
```
</details>
### Express + GraphQL Yoga
<details>
<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');
});
```
</details>
### NestJS Integration
<details>
<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);
});
}
}
```
</details>
### TypeGraphQL Integration
<details>
<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>
);
}
```
</details>
### Image Upload with Validation
<details>
<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');
});
```
</details>
## 📖 API Documentation
### Middleware Functions
#### `graphqlUploadExpress(options?)`
Express middleware for handling multipart/form-data requests.
```typescript
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.
```typescript
import { graphqlUploadKoa } from 'graphql-upload-ts';
app.use(graphqlUploadKoa({
maxFileSize: 10000000, // 10 MB
maxFiles: 10,
}));
```
### Types
#### `FileUpload`
The promise returned from uploaded files contains:
```typescript
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:
```typescript
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:
```typescript
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
```typescript
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:
```typescript
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`](https://npm.im/busboy) parses multipart requests
2. **File Buffering** - Files are buffered to filesystem using [`fs-capacitor`](https://npm.im/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:
```typescript
// 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
```bash
# 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](./LICENSE) © Mohamed Meabed
## 🙏 Acknowledgments
This library is a TypeScript fork of [`graphql-upload`](https://github.com/jaydenseric/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
- [NPM Package](https://www.npmjs.com/package/graphql-upload-ts)
- [GitHub Repository](https://github.com/meabed/graphql-upload-ts)
- [GraphQL Multipart Request Spec](https://github.com/jaydenseric/graphql-multipart-request-spec)
- [Issue Tracker](https://github.com/meabed/graphql-upload-ts/issues)
- [Examples Directory](https://github.com/meabed/graphql-upload-ts/tree/master/examples)
---
<div align="center">
Made with ❤️ by <a href="https://github.com/meabed">Mohamed Meabed</a>
</div>