
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: May 17, 2025
In this tutorial, we’ll build a small application using Apache Camel to expose both GraphQL and REST APIs. Apache Camel is a powerful integration framework that simplifies connecting different systems, including APIs, databases, and messaging services.
First, in our pom.xml file, let’s add dependencies for Camel core, Jetty, GraphQL, and Jackson:
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-core</artifactId>
<version>4.11.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>23.1</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-jetty</artifactId>
<version>4.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-graphql</artifactId>
<version>4.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.0</version>
</dependency>
These dependencies bring in the necessary components to build the application. camel-jetty allows us to expose HTTP endpoints using Jetty as the embedded web server, while camel-graphql is used for serving GraphQL queries.
Lastly, Jackson is needed to handle JSON serialization and deserialization.
We’ll create a simple model class called Book. This class represents the data we want to return in our APIs:
public class Book {
private String id;
private String title;
private String author;
// constructor, getter and setter methods
}
Next, we create a BookService class that returns a list of books. We’re mocking the data for simplicity:
public class BookService {
private final List<Book> books = new ArrayList<>();
public BookService() {
books.add(new Book("1", "Clean Code", "Robert"));
books.add(new Book("2", "Effective Java", "Joshua"));
}
public List<Book> getBooks() {
return books;
}
public Book getBookById(String id) {
return books.stream().filter(b -> b.getId().equals(id)).findFirst().orElse(null);
}
public Book addBook(Book book) {
books.add(book);
return book;
}
}
This service provides three main operations: retrieve all books, get a book by ID, and add a new book.
We’ll expose RESTful endpoints using Apache Camel with Jetty as the embedded web server. Camel simplifies defining HTTP endpoints and routing logic through a RouteBuilder.
Let’s begin by defining a route in a class called BookRoute:
public class BookRoute extends RouteBuilder {
private final BookService bookService = new BookService();
@Override
public void configure() {
onException(Exception.class)
.handled(true)
.setHeader("Content-Type", constant("application/json"))
.setBody(simple("{\"error\": \"${exception.message}\"}"));
restConfiguration()
.component("jetty")
.host("localhost")
.port(8080)
.contextPath("/api")
.bindingMode(RestBindingMode.json);
//...
}
}
The configure() method is where all route definitions live. Camel calls this during initialization to build the processing pipeline. We use the onException() method to create a global exception handler that catches any unhandled exceptions thrown during request processing.
restConfiguration() defines the server settings. We use Jetty on port 8080 and set the binding mode to JSON so that Camel automatically converts Java objects to JSON responses.
The host and port settings determine where our API will be accessible, while the context path establishes a base URL prefix for all our endpoints.
Next, we’ll create three endpoints for managing Book resources:
Let’s define our actual REST endpoints using rest():
rest("/books")
.get().to("direct:getAllBooks")
.get("/{id}").to("direct:getBookById")
.post().type(Book.class).to("direct:addBook");
We now define the internal Camel routes for each operation:
These connect the direct:* endpoints to the BookService methods:
from("direct:getAllBooks")
.bean(bookService, "getBooks");
from("direct:getBookById")
.bean(bookService, "getBookById(${header.id})");
from("direct:addBook")
.bean(bookService, "addBook");
By leveraging Apache Camel’s fluent DSL, we’ve cleanly separated HTTP routing from business logic and provided a maintainable and extensible way to expose REST APIs.
To add GraphQL support to our application, we start by defining the schema in a separate file named books.graphqls. This file uses the GraphQL Schema Definition Language (SDL), which allows us to describe the structure of our API in a simple, declarative format:
type Book {
id: String!
title: String!
author: String
}
type Query {
books: [Book]
bookById(id: String!): Book
}
type Mutation {
addBook(id: String!, title: String!, author: String): Book
}
The schema begins with a Book type, which represents the main entity in our system. It contains three fields: id, title, and author. The id and title fields are annotated with an exclamation mark (!), indicating that these fields are non-nullable.
Following the type definition, the Query type outlines the operations clients can perform to retrieve data. Specifically, it allows clients to fetch a list of all books using the books query or to retrieve a single book by its ID using the bookById query.
To allow clients to create new book entries, we add a Mutation type. The addBook mutation accepts two arguments—title (required) and author (optional)—and returns the newly created Book object.
Now we need to create a schema loader class that connects GraphQL queries to the Java service. We create a class called CustomSchemaLoader, which loads the schema, parses it into a registry, and defines how each GraphQL operation is resolved using data fetchers:
public class CustomSchemaLoader{
private final BookService bookService = new BookService();
public GraphQLSchema loadSchema() {
try (InputStream schemaStream = getClass().getClassLoader().getResourceAsStream("books.graphqls")) {
if (schemaStream == null) {
throw new RuntimeException("GraphQL schema file 'books.graphqls' not found in classpath");
}
TypeDefinitionRegistry registry = new SchemaParser()
.parse(new InputStreamReader(schemaStream));
RuntimeWiring wiring = buildRuntimeWiring();
return new SchemaGenerator().makeExecutableSchema(registry, wiring);
} catch (Exception e) {
logger.error("Failed to load GraphQL schema", e);
throw new RuntimeException("GraphQL schema initialization failed", e);
}
}
public RuntimeWiring buildRuntimeWiring() {
return RuntimeWiring.newRuntimeWiring()
.type("Query", builder -> builder
.dataFetcher("books", env -> bookService.getBooks())
.dataFetcher("bookById", env -> bookService.getBookById(env.getArgument("id"))))
.type("Mutation", builder -> builder
.dataFetcher("addBook", env -> {
String id = env.getArgument("id");
String title = env.getArgument("title");
String author = env.getArgument("author");
if (title == null || title.isEmpty()) {
throw new IllegalArgumentException("Title cannot be empty");
}
return bookService.addBook(new Book(id, title, author));
}))
.build();
}
}
The dataFetcher() serves as a connector between the GraphQL queries or mutations and the actual methods in our service layer. For example, when a client queries books, the system internally calls bookService.getBooks().
Similarly, for the addBook mutation, the resolver extracts the title and author arguments from the request and passes them to the bookService.addBook(title, author).
With our schema and service logic in place, the next step is to expose our GraphQL endpoint using Apache Camel. To achieve this, we define a Camel route that listens for incoming HTTP POST requests and delegates them to the GraphQL engine for processing.
We configure the route using the Jetty component to listen for HTTP requests at port 8080, specifically on the /graphql path:
from("jetty:http://localhost:8088/graphql?matchOnUriPrefix=true")
.log("Received GraphQL request: ${body}")
.convertBodyTo(String.class)
.process(exchange -> {
String body = exchange.getIn().getBody(String.class);
try {
Map<String, Object> payload = new ObjectMapper().readValue(body, Map.class);
String query = (String) payload.get("query");
if (query == null || query.trim().isEmpty()) {
throw new IllegalArgumentException("Missing 'query' field in request body");
}
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(query)
.build();
ExecutionResult result = graphQL.execute(executionInput);
Map<String, Object> response = result.toSpecification();
exchange.getIn().setBody(response);
} catch (Exception e) {
throw new RuntimeException("GraphQL processing error", e);
}
})
.marshal().json(JsonLibrary.Jackson)
.setHeader(Exchange.CONTENT_TYPE, constant("application/json"));
The route listens for HTTP POST requests sent to /graphql. It extracts the query field from the incoming payload and executes it against the loaded GraphQL schema. The result of the query is transformed into a standard GraphQL response format, followed by marshaled back into JSON.
Now that the route is defined, we need a main class to bootstrap the application. This class is responsible for initializing the Camel context, registering the schema loader, adding routes, and keeping the server alive.
We create a class called CamelRestGraphQLApp with a main method that performs the necessary setup:
public class CamelRestGraphQLApp {
public static void main(String[] args) throws Exception {
CamelContext context = new DefaultCamelContext();
context.addRoutes(new BookRoute());
context.start();
logger.info("Server running at http://localhost:8080");
Thread.sleep(Long.MAX_VALUE);
context.stop();
}
}
This class registers the schema loader as a bean, adds the routes, and starts the server.
The Thread.sleep(LONG.MAX_VALUE) call is a simple way to keep the application running. In a production-grade application, this would be replaced with a more robust mechanism for managing the application lifecycle, but for demonstration purposes, this keeps the server alive to handle incoming requests.
We can write a test using Camel’s CamelContext and the ProducerTemplate to simulate sending HTTP requests:
@Test
void whenCallingRestGetAllBooks_thenShouldReturnBookList() {
String response = template.requestBodyAndHeader(
"http://localhost:8080/api/books",
null,
Exchange.CONTENT_TYPE,
"application/json",
String.class
);
assertNotNull(response);
assertTrue(response.contains("Clean Code"));
assertTrue(response.contains("Effective Java"));
}
@Test
void whenCallingBooksQuery_thenShouldReturnAllBooks() {
String query = """
{
"query": "{ books { id title author } }"
}""";
String response = template.requestBodyAndHeader(
"http://localhost:8080/graphql",
query,
Exchange.CONTENT_TYPE,
"application/json",
String.class
);
assertNotNull(response);
assertTrue(response.contains("Clean Code"));
assertTrue(response.contains("Effective Java"));
}
In this article, we successfully integrated both REST and GraphQL endpoints into our Camel application, enabling efficient management of book data through both query-based and mutation-based APIs.
As always, the source code is available over on GitHub.