The middleware and Upload scalar in this package enable GraphQL multipart requests (file uploads via queries and mutations) in your Apollo Server Next.js integration.
npm install graphql-upload-nextjsgraphql-upload-nextjs is a robust package that enables seamless file uploads in a Next.js environment using GraphQL. This package is designed to integrate easily with Apollo Server, allowing you to handle file uploads in your GraphQL mutations with ease and efficiency.
bash
npm install graphql-upload-nextjs
or
yarn add graphql-upload-nextjs
or
pnpm add graphql-upload-nextjs
`
Usage
$3
Import the necessary components from the package:
`javascript
import { GraphQLUpload, type File, uploadProcess } from 'graphql-upload-nextjs'
import { ApolloServer } from '@apollo/server'
import { NextRequest } from 'next/server'
import { createWriteStream } from 'fs'
import { pipeline } from 'stream'
import { startServerAndCreateNextHandler } from '@as-integrations/next'
// Optional: Use the gql module from Apollo Client for syntax highlighting.
// This package is already installed for the client side.
import { gql } from '@apollo/client'
`
$3
Define your GraphQL schema and resolvers:
`javascript
// For this example, we define the GraphQL schema and resolvers below.
const typeDefs = gql
const resolvers = {
Mutation: {
uploadFile: async (
_parent: undefined,
{ file }: { file: Promise },
_context: Context,
): Promise => {
try {
const { createReadStream, encoding, fileName, fileSize, mimeType } =
await file;
const allowedTypes = ["image/jpeg", "image/png", "text/plain"];
const maxFileSize = 10 1024 1024; // 10MB
if (!allowedTypes.includes(mimeType)) {
throw new Error( File type ${mimeType} is not allowed.);
}
if (fileSize > maxFileSize) {
throw new Error(File size exceeds the limit of 10MB.);
}
return new Promise((resolve, reject) => {
pipeline(
createReadStream(),
// IMPORTANT: Storing files in 'public' is insecure for production. Use secure storage.
createWriteStream( ./public/${fileName}),
(error) => {
if (error) {
reject(new Error("Error during file upload."));
} else {
resolve({
encoding,
fileName,
fileSize,
mimeType,
uri: http://localhost:3000/${fileName},
});
}
},
);
});
} catch (_error) {
throw new Error("Failed to handle file upload.");
}
},
uploadFiles: async (
_parent: undefined,
{ files }: { files: Promise[] },
_context: Context,
): Promise => {
const resolvedFileObjects = await Promise.all(files);
const allowedTypes = ["image/jpeg", "image/png", "text/plain"];
const maxFileSize = 10 1024 1024; // 10MB
const processingPromises = resolvedFileObjects.map(async (fileObject) => {
if (
!fileObject ||
typeof fileObject.createReadStream !== "function" ||
!fileObject.fileName
) {
throw new Error(
Invalid file data encountered for one of the files.,
);
}
const { createReadStream, encoding, fileName, fileSize, mimeType } =
fileObject;
if (!allowedTypes.includes(mimeType)) {
throw new Error(
File type ${mimeType} is not allowed for ${fileName}.,
);
}
if (fileSize > maxFileSize) {
throw new Error(File ${fileName} size exceeds the limit of 10MB.);
}
return new Promise((resolve, reject) => {
const readStream = createReadStream();
if (typeof readStream.pipe !== "function") {
return reject(
new Error( Failed to get a readable stream for ${fileName}.),
);
}
pipeline(
readStream,
// IMPORTANT: Insecure storage. Use secure solution in production.
createWriteStream(./public/${fileName}),
(error) => {
if (error) {
reject(new Error(Error during upload of ${fileName}.));
} else {
resolve({
encoding,
fileName,
fileSize,
mimeType,
uri: http://localhost:3000/${fileName},
});
}
},
);
});
});
return Promise.all(processingPromises);
},
},
Query: {
default: async () => true
},
// Add the custom scalar type for file uploads.
Upload: GraphQLUpload
}
`
$3
Create the Apollo Server instance and set up the request handler:
`javascript
const server = new ApolloServer({ resolvers, typeDefs });
interface Context {
ip: string;
req: NextRequest;
[key: string]: unknown;
}
const contextHandler = async (
req: NextRequest,
authenticated: string | boolean = false,
): Promise => {
const ip = req.headers.get("x-forwarded-for") || "";
if (authenticated) return { ip, req };
return { ip, req };
};
interface ServerExecuteOperationParams {
query: string;
variables: Record;
}
interface ExpectedServerType> {
executeOperation: (
params: ServerExecuteOperationParams,
context: { contextValue: TContext },
) => Promise>;
}
const handler = startServerAndCreateNextHandler(server, {
context: contextHandler,
});
const requestHandler = async (request: NextRequest) => {
try {
if (request.headers.get("content-type")?.includes("multipart/form-data")) {
// IMPORTANT: Authenticate before processing uploads. Placeholder 'User' used.
const context = await contextHandler(request, "User");
return await uploadProcess(
request,
context,
server as ExpectedServerType,
);
}
return handler(request);
} catch (_error) {
throw new Error("Failed to process request.");
}
};
// Export request handlers for GET, POST, and OPTIONS methods.
export const GET = requestHandler;
export const POST = requestHandler;
export const OPTIONS = requestHandler;
`
$3
When sending requests to your GraphQL server, you'll need to structure your mutation and variables correctly. This package adheres to the GraphQL multipart request specification for file uploads.
Single File Upload (uploadFile mutation):
GraphQL Operation:
`graphql
mutation UploadFile($file: Upload!) {
uploadFile(file: $file) {
fileName
mimeType
encoding
uri
fileSize
}
}
`
GraphQL Variables:
The key in the variables object ("file") must match the argument name in your GraphQL mutation ($file).
When using a GraphQL client library (e.g., Apollo Client, urql, Relay):
* You'll typically pass the browser's File object (e.g., from an ) directly as the value for the file variable.
* The client library automatically constructs the multipart/form-data request according to the GraphQL multipart request specification.
The {"file": null} structure illustrates how the operations part of the multipart request is formed, where null acts as a placeholder for the actual file content that is sent in a separate part of the request. You generally don't need to construct this manually when using a client library.
Example with a client library (conceptual):
`javascript
// In your frontend code
import { gql, useMutation } from '@apollo/client'; // Or your client of choice
const UPLOAD_FILE_MUTATION = gql
;
function MyUploader() {
const [uploadFileMutation] = useMutation(UPLOAD_FILE_MUTATION);
const handleChange = (event) => {
const file = event.target.files[0];
if (file) {
uploadFileMutation({ variables: { file } });
}
};
return ;
}
`
Multiple File Upload (uploadFiles mutation):
GraphQL Operation:
`graphql
mutation UploadFiles($files: [Upload!]!) {
uploadFiles(files: $files) {
fileName
mimeType
encoding
uri
fileSize
}
}
`
GraphQL Variables:
Similarly, the key "files" must match the argument name ($files).
When using a GraphQL client library:
* You'll pass an array of File objects as the value for the files variable.
* The client library handles the multipart request construction.
The {"files": [null, null]} structure illustrates the operations part, with null placeholders for file content sent separately.
Example with a client library (conceptual):
`javascript
// In your frontend code
const UPLOAD_FILES_MUTATION = gql
;
function MyMultiUploader() {
const [uploadFilesMutation] = useMutation(UPLOAD_FILES_MUTATION);
const handleChange = (event) => {
const files = Array.from(event.target.files);
if (files.length > 0) {
uploadFilesMutation({ variables: { files } });
}
};
return ;
}
`
Example
An example project demonstrating how to integrate GraphQL file uploads into a typical Next.js starter application is available in the repository under graphql-upload-nextjs/examples/example-graphql-upload-nextjs/`.