Handling file uploads can sometimes feel complex, but with the right tools, it’s surprisingly straightforward. If you’re using React and GraphQL with Apollo Client, you can build a robust file upload component with just a few key pieces.
Let’s break down how to set up your Apollo Client and create a reusable React component to handle file uploads seamlessly.
Step 1: Configuring Apollo Client for File Uploads
Before our React component can send a file, our Apollo Client needs to know how to handle it. Standard GraphQL requests use JSON, but files need to be sent as multipart/form-data. This is where the apollo-upload-client library comes in handy.
import { ApolloClient, ApolloLink, InMemoryCache } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import createUploadLink from 'apollo-upload-client/createUploadLink.mjs';
// The URL of your GraphQL server
const API_URI = "http://localhost:8080/graphql";
// This is the special link that handles file uploads.
// It creates a multipart request when a file is present.
const uploadLink = createUploadLink({ uri: API_URI });
// This link helps catch and log errors, which is great for debugging.
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message }) =>
console.error(`[GraphQL error]: Message: ${message}`)
);
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
}
});
// We combine the links into a single chain.
// The request will flow through errorLink and then to uploadLink.
export const apolloClient = new ApolloClient({
link: ApolloLink.from([
errorLink,
uploadLink // This must be the terminating link!
]),
cache: new InMemoryCache(),
});
What’s happening here?
createUploadLink: This is the most important part. Instead of the standard createHttpLink, we use createUploadLink. It intelligently detects if your GraphQL mutation includes a file. If it does, it sends the request as multipart/form-data; otherwise, it sends a normal application/json request.
errorLink: This is a helpful piece of middleware that intercepts errors from your requests. It logs any GraphQL or network errors to the console, making it much easier to find and fix bugs.
ApolloLink.from([...]): Apollo Client lets you chain "links" together. A request flows through each link in the array, allowing you to add functionality like error handling, authentication, or logging before the request is finally sent to the server by the last link in the chain (uploadLink).
Step 2: Building the File Upload Component in React
Now for the fun part: the user-facing component! We’ll create a simple component that contains a file input and uses an Apollo Client mutation hook to send the selected file to our backend.
Here’s the generalized FileUpload component:
import React from 'react';
// This hook would be auto-generated by GraphQL Code Generator
import { useUploadFileMutation } from '../generated/graphql';
import { toast } from 'react-toastify';
// Props for our component
interface FileUploadProps {
//Link Image to your Entity(Example image may belong to product1)
linkedEntityId: number;
// A function to call after a successful upload,
onUploadSuccess: () => void;
}
export const FileUpload: React.FC<FileUploadProps> = ({ linkedEntityId, onUploadSuccess }) => {
// 1. Get the mutation function and loading state from the Apollo hook
const [uploadFile, { loading }] = useUploadFileMutation();
// 2. This function runs when the user selects a file
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
// Get the first file from the input
const file = event.target.files?.[0];
if (!file) return; // Exit if no file is selected
try {
// 3. Execute the mutation, passing the file and the ID as variables
const { data } = await uploadFile({
variables: {
linkedEntityId: linkedEntityId, // Use the prop value here
file: file,
},
});
// 4. Handle the response from the server
if (data?.uploadFile?.success) {
toast.success('File uploaded successfully!');
onUploadSuccess(); // Call the success callback
} else {
// Show an error message from the server or a generic one
toast.error(data?.uploadFile?.message || 'Failed to upload file.');
}
} catch (error) {
console.error("Upload error:", error);
toast.error('An error occurred during the upload.');
}
};
// 5. Render the UI
return (
<div>
<input
type="file"
accept="image/png, image/jpeg" // Specify accepted file types
onChange={handleFileChange}
disabled={loading} // Disable the input while uploading
/>
{loading && <p>Uploading...</p>}
useUploadFileMutation: This is a custom hook provided by Apollo Client (typically auto-generated) that connects to your GraphQL uploadFile mutation. It returns an array containing the mutation function (uploadFile) and an object with status information like loading.
handleFileChange: This event handler is triggered when a user selects a file. It grabs the file from the browser event.
Executing the Mutation: The core of the logic is executing the uploadFile function returned by our hook. We call it with a variables object containing everything our GraphQL mutation needs—in this case, both the file and the linkedEntityId. Our configured uploadLink in Apollo Client sees the file object and automatically handles the multipart request.
Feedback Loop: After the upload, we check the response. We use a library like react-toastify to show a success or error message. We also call the onUploadSuccess function passed in via props. This is a powerful pattern that lets a parent component know the upload is done, so it can, for example, refresh a list of images.
The JSX: The user interface is simple. An lets the user pick a file. We disable it and show a "Uploading..." message when the loading state is true to prevent multiple submissions and give the user clear feedback.
Mutation
I Used codegen. It connects to your backend to fetch the full API schema and reads your .graphql file. Based on both, it automatically generates the corresponding frontend code, such as the useUploadFileMutation React hook, ready for you to import and use.
mutation UploadFile($file: Upload!, $linkedEntityId: Int!) {
uploadFile(file: $file, linkedEntityId: $linkedEntityId) {
success
message
url
}
}
Backend(GraphQl + .Net + SQL)
Step 1: The Foundation — Our Database Table
Everything starts with a place to store the data. For our file upload, we need a database table to hold the image itself, its type, and a way to link it to whatever it belongs to — be it a blog post, a user profile, or a product.
Here’s a simple SQL schema for a table that could store our images. Notice we’re using a generic name like LinkedEntityId instead of ProductId to make it reusable.
dbo.Images Table Schema
CREATE TABLE Images (
Id INT PRIMARY KEY IDENTITY(1,1),
ImageData VARBINARY(MAX) NOT NULL, -- The actual file data
MimeType NVARCHAR(100) NOT NULL, -- e.g., 'image/png', 'image/jpeg'
LinkedEntityId INT NOT NULL -- The ID of the item this image is for
);
Step 2: The Repository — Talking to the Database
The Repository Layer has one job: to handle all direct database communication. It abstracts away the raw SQL queries. This means our business logic won’t have messy database code mixed in.
Here’s a simplified C# repository method for inserting an image into our new table.
ImageRepository.cs
public class ImageRepository
{
private readonly string _connectionString;
public ImageRepository(string connectionString)
{
_connectionString = connectionString;
}
// A simple method to add a new image record
public async Task<int> AddImageAsync(byte[] imageData, string mimeType, int linkedEntityId)
{
using (var connection = new SqlConnection(_connectionString))
{
var sql = @"
INSERT INTO Images (ImageData, MimeType, LinkedEntityId)
VALUES (@ImageData, @MimeType, @LinkedEntityId);
SELECT CAST(SCOPE_IDENTITY() AS INT);"; // Return the new ID
return await connection.QuerySingleAsync<int>(sql, new {
ImageData = imageData,
MimeType = mimeType,
LinkedEntityId = linkedEntityId
});
}
}
This code defines a single, clean method AddImageAsync that takes exactly what it needs to create a new row in the Images table.
Step 3: The Service — Handling the Business Logic
The Service Layer is the brain of the operation. It coordinates the work, contains the business logic, and decides which repository methods to call. It doesn’t know how the data is saved, only that it needs to be saved.
Our ImageService will take the uploaded file, process it, and then use our repository.
ImageService.cs
public class ImageService
{
private readonly ImageRepository _imageRepository;
public ImageService(ImageRepository imageRepository)
{
_imageRepository = imageRepository;
}
// The main logic for handling the file upload
public async Task<FileUploadResponse> UploadImageAsync(IFormFile file, int linkedEntityId)
{
if (file == null || file.Length == 0)
{
return new FileUploadResponse { Success = false, Message = "No file provided." };
}
// Convert the uploaded file into a byte array
byte[] imageData;
using (var memoryStream = new MemoryStream())
{
await file.CopyToAsync(memoryStream);
imageData = memoryStream.ToArray();
}
// Call the repository to save the data
try
{
var newImageId = await _imageRepository.AddImageAsync(imageData, file.ContentType, linkedEntityId);
// In a real app, you'd generate a URL here
return new FileUploadResponse { Success = true, Message = "Upload successful!", Url = $"/images/{newImageId}" };
}
catch (Exception ex)
{
// Log the error
return new FileUploadResponse { Success = false, Message = "Failed to save image." };
}
}
}
Notice the clear separation: the service handles the IFormFile object (a web concept), converts it to a simple byte[], and then passes that raw data to the repository.
Step 4: The GraphQL Resolver — The Entry Point
Finally, the GraphQL Resolver is the entry point for our frontend’s API call. Its job is to parse the incoming request, call the appropriate service method, and return a response.
Mutation.cs (GraphQL Resolver)
public class Mutation
{
private readonly ImageService _imageService;
public Mutation(ImageService imageService)
{
_imageService = imageService;
}
// This method maps directly to our 'uploadFile' mutation in the schema
public async Task<FileUploadResponse> UploadFile(IFormFile file, int linkedEntityId)
{
// The resolver's job is simple: delegate the work to the service layer
return await _imageService.UploadImageAsync(file, linkedEntityId);
}
}
And that’s it! The resolver is thin and clean. It receives the arguments from the GraphQL mutation (file and linkedEntityId) and immediately passes them to the service layer to do the heavy lifting.
Top comments (0)