Upload Files With GraphQL in Java
File uploads are a common feature in modern applications, and GraphQL can handle them with some customization. GraphQL doesn’t natively support file uploads out of the box, and managing file uploads in a GraphQL API involves a few additional steps compared to traditional REST APIs. This article will guide us through creating a Spring Boot application that enables file uploads via GraphQL.
1. Introduction to File Upload with GraphQL
Standard GraphQL doesn’t support file uploads natively. GraphQL typically works with JSON-based queries, and files like images or documents don’t fit into this format. However, we can work around this limitation by using the multipart form data format, which allows files to be transmitted alongside the GraphQL query in a single request.
By defining a custom scalar type called Upload
, we can handle file uploads within the GraphQL framework, making it easy to integrate file handling into our GraphQL API.
2. Dependencies
In this section, we’ll add the required dependencies to set up our GraphQL application and enable file uploads. Set up a Spring Boot project which can be done by using Spring Initializr to create a project with the following necessary dependencies:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-graphql</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.graphql</groupId> <artifactId>spring-graphql-test</artifactId> <scope>test</scope> </dependency>
2. Define Schema for File Upload
In GraphQL, scalars are used to define custom data types. Here, we define a Upload
scalar to represent file uploads. Create a new file called src/main/resources/graphql/mutation.graphqls
and add the following content to define the upload operation.
This GraphQL schema defines the structure for a simple file upload mutation.
scalar Upload type Mutation { uploadFile(file: Upload!): String } type Query { getFile: String }
In this code, the uploadFile
mutation expects one parameter: a file of type Upload
. The custom scalar type Upload
will be mapped to a MultipartFile
, allowing us to process the file upload.
3. Handle Upload Scalar with Coercing
In the context of GraphQL, coercing refers to the process of converting data between the format used by the GraphQL schema and the format received or sent in the API requests and responses. This is important for custom scalar types or complex data formats.
To handle the Upload
scalar, we need to implement a custom coercing class that manages how the MultipartFile
object is processed.
import graphql.schema.Coercing; import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; import graphql.schema.CoercingSerializeException; import org.springframework.web.multipart.MultipartFile; public class UploadCoercing implements Coercing<MultipartFile, MultipartFile> { @Override public MultipartFile serialize(Object dataFetcherResult) throws CoercingSerializeException { throw new CoercingSerializeException("Upload is an input-only type"); } @Override public MultipartFile parseValue(Object input) throws CoercingParseValueException { if (input instanceof MultipartFile) { return (MultipartFile) input; } throw new CoercingParseValueException( String.format("Expected a 'MultipartFile' like object but was '%s'.", input != null ? input.getClass() : null) ); } @Override public MultipartFile parseLiteral(Object input) throws CoercingParseLiteralException { throw new CoercingParseLiteralException("Parsing literal of 'MultipartFile' is not supported"); } }
The UploadCoercing
class defines how the Upload
scalar is interpreted. The parseValue
method ensures that the input is of type MultipartFile
, and it throws exceptions for unsupported operations like serialization and parsing literals. This coercing logic is essential to safely process file uploads and prevent unsupported operations.
4. Handling Multipart Requests
This section contains the GraphqlMultipartHandler
class, which processes multipart form-data requests, extracts file uploads, and maps them to GraphQL query variables.
public class GraphqlMultipartHandler { // A handler to process GraphQL requests private final WebGraphQlHandler graphQlHandler; // ObjectMapper used to convert between JSON and Java objects private final ObjectMapper objectMapper; // Constructor that initializes the handler and object mapper with null checks public GraphqlMultipartHandler(WebGraphQlHandler graphQlHandler, ObjectMapper objectMapper) { Assert.notNull(graphQlHandler, "WebGraphQlHandler is required"); Assert.notNull(objectMapper, "ObjectMapper is required"); this.graphQlHandler = graphQlHandler; this.objectMapper = objectMapper; } // Supported media types for the GraphQL response (GraphQL and JSON) public static final List<MediaType> SUPPORTED_RESPONSE_MEDIA_TYPES = Arrays.asList(MediaType.APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON); private static final Log logger = LogFactory.getLog(GraphQlHttpHandler.class); private final IdGenerator idGenerator = new AlternativeJdkIdGenerator(); // Main method to handle the incoming multipart GraphQL request public ServerResponse handleRequest(ServerRequest serverRequest) throws ServletException { // Read the 'operations' parameter from the request Optional<String> operation = serverRequest.param("operations"); // Read the 'map' parameter which maps files to query variables Optional<String> mapParam = serverRequest.param("map"); // Parse the 'operations' JSON into a Map Map<String, Object> inputQuery = readJson(operation, new TypeReference<>() {}); // Extract query variables from the input query final Map<String, Object> queryVariables; if (inputQuery.containsKey("variables")) { queryVariables = (Map<String, Object>) inputQuery.get("variables"); } else { queryVariables = new HashMap<>(); } // Handle extensions if present in the request Map<String, Object> extensions = new HashMap<>(); if (inputQuery.containsKey("extensions")) { extensions = (Map<String, Object>) inputQuery.get("extensions"); } // Read the file parts from the multipart body Map<String, MultipartFile> fileParams = readMultipartBody(serverRequest); // Parse the 'map' JSON to associate files with variables Map<String, List<String>> fileMapInput = readJson(mapParam, new TypeReference<>() {}); // Map each file to its corresponding variable path in the GraphQL query fileMapInput.forEach((String fileKey, List<String> objectPaths) -> { MultipartFile file = fileParams.get(fileKey); if (file != null) { objectPaths.forEach((String objectPath) -> { MultipartVariableMapper.mapVariable( objectPath, queryVariables, file ); }); } }); // Extract the actual GraphQL query and operation name String query = (String) inputQuery.get("query"); String opName = (String) inputQuery.get("operationName"); // Remote address and cookies (if needed) for the request InetSocketAddress remoteAddress = null; MultiValueMap<String, HttpCookie> cookies = null; // Build the request body including query, variables, and extensions Map<String, Object> body = new HashMap<>(); body.put("query", query); body.put("operationName", StringUtils.hasText(opName) ? opName : ""); body.put("variables", queryVariables); body.put("extensions", extensions); // Create a WebGraphQlRequest that wraps all the information required for processing WebGraphQlRequest graphQlRequest = new WebGraphQlRequest(serverRequest.uri(), serverRequest.headers().asHttpHeaders(), cookies, remoteAddress, queryVariables, body, this.idGenerator.generateId().toString(), LocaleContextHolder.getLocale() ); if (logger.isDebugEnabled()) { logger.debug("Executing: " + graphQlRequest); } // Execute the request and return the server response as an asynchronous result Mono<ServerResponse> responseMono = this.graphQlHandler.handleRequest(graphQlRequest) .map(response -> { if (logger.isDebugEnabled()) { logger.debug("Execution complete"); } // Build the response with appropriate headers and content type ServerResponse.BodyBuilder builder = ServerResponse.ok(); builder.headers(headers -> headers.putAll(response.getResponseHeaders())); builder.contentType(selectResponseMediaType(serverRequest)); return builder.body(response.toMap()); }); // Return the async server response return ServerResponse.async(responseMono); } }
The GraphqlMultipartHandler
class is responsible for handling multipart file uploads in GraphQL requests. It integrates a WebGraphQlHandler
for processing the core GraphQL logic and an ObjectMapper
for JSON parsing. The main functionality involves reading the incoming request, extracting the GraphQL query and associated variables, mapping file uploads to the appropriate GraphQL query parameters, and finally, executing the request and returning the appropriate response.
The handler supports both GraphQL and JSON media types and is designed to handle multipart forms, mapping files to GraphQL variables via a map
parameter. The handleRequest
method reads the file and query from the multipart body, logs the request, and processes it asynchronously. It parses the operation and maps parameters from the request, ensuring that uploaded files are correctly assigned to the variables in the query. The response is then built using appropriate headers and media types.
The class also includes utility methods like readJson
for parsing JSON data and readMultipartBody
for handling the file content. The full source code of this class can be downloaded from the link provided at the end of this article.
5. File Upload Data Fetcher
In this part of the article, we create a DataFetcher
to manage the file upload mutation. The GraphQLFileUploader
works with the FileStorageService
to save the file and return the file path after a successful upload.
import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; @Component public class GraphQLFileUploader implements DataFetcher<String> { private final FileStorageService fileStorageService; public GraphQLFileUploader(FileStorageService fileStorageService) { this.fileStorageService = fileStorageService; } @Override public String get(DataFetchingEnvironment environment) { MultipartFile file = environment.getArgument("file"); String storedFilePath = fileStorageService.store(file); return String.format("File stored at: %s", storedFilePath); } }
The GraphQLFileUploader
class is a Spring component that handles file uploads in a GraphQL API by implementing the DataFetcher
interface. It retrieves the file from the DataFetchingEnvironment
during a GraphQL mutation and uses the FileStorageService
to store the file. In the get()
method, the file is extracted from the mutation request, passed to the FileStorageService
for saving, and a confirmation message with the file’s storage path is returned.
5.1 File Storage Service
In this section, we define the FileStorageService
class, which handles the storage of uploaded files.
import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Objects; @Service public class FileStorageService { private final Path rootLocation = Paths.get("uploads"); public FileStorageService() { try { Files.createDirectories(rootLocation); } catch (IOException e) { throw new RuntimeException("Failed to initialize the storage location.", e); } } public String store(MultipartFile file) { try { if (file.isEmpty()) { throw new RuntimeException("Failed to store empty file."); } Path destination = rootLocation.resolve( Paths.get(Objects.requireNonNull(file.getOriginalFilename()))) .normalize().toAbsolutePath(); file.transferTo(destination); return String.format("File uploaded successfully: %s", destination); } catch (IOException e) { throw new RuntimeException("Failed to store file.", e); } } }
The FileStorageService
class handles file storage in the application. It initializes with an uploads
directory and ensures it exists. If directory creation fails, an exception is thrown. The store
method saves the MultipartFile
to the specified directory (a directory named uploads
, which is located relative to the application’s working directory).
6. GraphQL Configuration
This section covers configuring the GraphQL server to recognize the Upload
scalar and route file uploads properly.
import com.fasterxml.jackson.databind.ObjectMapper; import static com.jcg.GraphqlMultipartHandler.SUPPORTED_RESPONSE_MEDIA_TYPES; import graphql.schema.GraphQLScalarType; import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; import org.springframework.boot.autoconfigure.graphql.GraphQlProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.graphql.execution.RuntimeWiringConfigurer; import org.springframework.graphql.server.WebGraphQlHandler; import org.springframework.http.MediaType; import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; import org.springframework.web.servlet.function.RequestPredicates; import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.RouterFunctions; import org.springframework.web.servlet.function.ServerResponse; @Configuration public class GraphqlConfiguration { private final GraphQLFileUploader fileUploadDataFetcher; public GraphqlConfiguration(GraphQLFileUploader fileUploadDataFetcher) { this.fileUploadDataFetcher = fileUploadDataFetcher; } @Bean public RuntimeWiringConfigurer runtimeWiringConfigurer() { return (builder) -> builder .type(newTypeWiring("Mutation").dataFetcher("uploadFile", fileUploadDataFetcher)) .scalar(GraphQLScalarType.newScalar() .name("Upload") .coercing(new UploadCoercing()) .build()); } @Bean @Order(1) public RouterFunction<ServerResponse> graphQlMultipartRouterFunction( GraphQlProperties properties, WebGraphQlHandler webGraphQlHandler, ObjectMapper objectMapper ) { String path = properties.getPath(); RouterFunctions.Builder builder = RouterFunctions.route(); GraphqlMultipartHandler graphqlMultipartHandler = new GraphqlMultipartHandler(webGraphQlHandler, objectMapper); builder = builder.POST(path, RequestPredicates.contentType(MULTIPART_FORM_DATA) .and(RequestPredicates.accept(SUPPORTED_RESPONSE_MEDIA_TYPES.toArray(MediaType[]::new))), graphqlMultipartHandler::handleRequest); return builder.build(); } }
The GraphqlConfiguration
class configures GraphQL for handling file uploads. The runtimeWiringConfigurer
method sets up a data fetcher for the uploadFile
mutation and defines a custom Upload
scalar type with the UploadCoercing
implementation. This method ensures that file upload operations are correctly wired in the GraphQL schema.
The graphQlMultipartRouterFunction
method establishes routing for multipart form-data requests. It creates a RouterFunction
that directs POST requests with MULTIPART_FORM_DATA
content to the GraphqlMultipartHandler
, which processes the file uploads. This setup enables efficient handling of file uploads in the GraphQL API.
7. Testing the File Upload API
We can test the file upload API using curl commands. Here’s an example curl request (replace the file path with your actual file location):
curl --location --request POST 'http://localhost:8080/graphql' --form 'operations={"query": "mutation UploadFile($file: Upload!) { uploadFile(file: $file) }", "variables": {"file": null}}' --form 'map={"file": ["variables.file"]}' --form 'file=@"/Users/omozegieaziegbe/Downloads/phoneicon.jpeg"'
The POST
request to the GraphQL endpoint is structured to handle file uploads using multipart form-data. The operations
form field specifies the GraphQL mutation UploadFile
, which requires a file
parameter. This field includes the GraphQL query and the variables for the mutation, detailing what operation to perform and which data to use.
The map
form field links the uploaded file to the mutation variable variables.file
, ensuring the server correctly associates the file with the mutation parameter. The file
form field includes the actual file data, provided via a file path. Together, these components enable the file to be sent and processed correctly by the GraphQL server.
Upon a successful file upload POST request, the response will confirm the file was uploaded successfully. Here’s what to expect in the output:
8. Conclusion
In this article, we explored how to implement file upload functionality in a Java GraphQL application using Spring Boot. We covered handling multipart requests and creating a DataFetcher to manage file uploads. By the end, we demonstrated how to test the implementation using a sample curl request. With these steps, we can incorporate file uploads into our own GraphQL APIs.
9. Download the Source Code
This article focuses on implementing Java GraphQL upload file functionality.
You can download the full source code of this example here: java graphql upload file