- Overview
- MicroProfile Features Implemented
- Prerequisites
- Building the Application
- Running the Application
- Example GraphQL Queries
- Mutations
- GraphQL Variables
- Aliases
- Fragments
- Error Handling
- Using with curl
- Schema Introspection
- Project Structure
- Key Implementation Details
- Development Tips
- Stopping the Application
This MicroProfile GraphQL Catalog Service provides a production-ready GraphQL API for product catalog management as per MicroProfile GraphQL 2.0 specification.
Key features demonstrated:
-
GraphQL queries and mutations for data retrieval and modification
-
Field resolvers using
@Sourcefor on-demand relationship resolution -
Computed fields for derived data (tax calculations, price categories, stock status)
-
Custom error handling with GraphQL-compliant responses
-
MicroProfile Config integration for externalized configuration
The application exposes a GraphQL API at /graphql with the following capabilities:
-
products- Retrieve all products -
product(id)- Get a specific product by ID -
searchProducts(searchTerm, category)- Search products -
productCount- Get total number of products -
averagePrice- Get average product price -
categories- Get all categories
-
createProduct(input)- Create a new product -
updateProduct(id, input)- Update an existing product -
deleteProduct(id)- Delete a product
Run the application using Liberty Maven plugin.
This project includes Liberty configuration at src/main/liberty/config/server.xml.
The Liberty Maven plugin uses this file automatically.
Start the server in development mode:
mvn liberty:devOr start in normal mode:
mvn liberty:startThe application will start on:
-
HTTP:
http://localhost:5060 -
Context root:
/graphql-catalog
The GraphQL endpoint is available at:
http://localhost:5060/graphql-catalog/graphql
A modern GraphiQL interface is included in the application and is accessible at:
http://localhost:5060/graphql-catalog/graphql-ui.html
This page uses React 18 and GraphiQL 3 loaded from unpkg.com and works in both local and GitHub Codespaces environments.
|
Note
|
Open Liberty also has a built-in GraphQL UI endpoint at /graphql-catalog/graphql-ui (enabled via io.openliberty.enableGraphQLUI in server.xml), but it has a known issue where the browser displays raw HTML source instead of rendering the UI due to a missing Content-Type: text/html header in the Liberty-bundled servlet. Use the /graphql-catalog/graphql-ui.html path above instead.
|
Port 5060 is pre-configured in the devcontainer as a public forwarded port. In the VS Code Ports tab, find port 5060 and open the forwarded address, then append /graphql-catalog/graphql-ui.html:
https://<codespace-name>-5060.app.github.dev/graphql-catalog/graphql-ui.html
This section provides example GraphQL queries and mutations that you can run against the GraphQL Catalog API at http://localhost:5060/graphql-catalog/graphql.
Retrieves the full list of products in the catalog. Use this query to display a product listing or to pre-populate a UI with basic product data.
query {
products {
id
name
price
category
stockQuantity
}
}Fetches complete details for a single product by its unique identifier. Useful for product detail pages where you need a specific product’s full data.
query {
product(id: 1) {
id
name
description
price
category
stockQuantity
}
}Retrieves a product with server-side computed fields. priceWithTax applies an 8% tax rate, priceCategory classifies the product as Budget, Mid-range, or Premium, and availabilityStatus reflects current stock levels (In Stock, Low Stock, or Out of Stock).
query {
product(id: 1) {
id
name
price
priceWithTax
priceCategory
availabilityStatus
}
}Fetches a product together with all its customer reviews and a computed average rating. The reviews field is resolved by a @Source field resolver that loads reviews on demand for the specified product.
query {
product(id: 1) {
id
name
price
reviews {
id
reviewerName
rating
comment
createdAt
}
averageRating
}
}Retrieves all products along with their associated reviews in a single request. Each product’s reviews field is resolved individually by the @Source field resolver.
query {
products {
id
name
price
reviews {
reviewerName
rating
comment
}
}
}Fetches a product’s highest-rated reviews up to a configurable limit. The topReviews field resolver accepts a limit argument (default: 5) and returns reviews sorted by rating descending — ideal for surfacing the best customer feedback.
query {
product(id: 1) {
id
name
topReviews(limit: 3) {
reviewerName
rating
comment
}
}
}Searches the catalog for products whose name or description matches a keyword, with an optional category filter. Both arguments are optional — omit category to search across all categories.
query {
searchProducts(searchTerm: "laptop", category: "Electronics") {
id
name
price
stockQuantity
}
}Searches for products by keyword without restricting results to a specific category.
query {
searchProducts(searchTerm: "mouse") {
id
name
price
}
}Returns aggregate statistics about the catalog: the total number of products, the mean price across all products, and a deduplicated list of all product categories.
query {
productCount
averagePrice
categories
}A single request that combines all query capabilities — basic fields, computed fields, field resolver data (reviews, average rating, top reviews), and catalog-level statistics. Demonstrates GraphQL’s strength in replacing multiple REST calls with one efficient network round-trip.
query {
products {
id
name
description
price
priceWithTax
priceCategory
availabilityStatus
category
stockQuantity
reviews {
reviewerName
rating
comment
}
averageRating
topReviews(limit: 2) {
rating
comment
}
}
productCount
averagePrice
categories
}Creates a new product in the catalog. The server auto-generates the id. The response can include computed fields so you can confirm the persisted state without an extra query.
mutation {
createProduct(input: {
name: "Wireless Charger"
description: "Fast wireless charging pad"
price: 39.99
category: "Electronics"
stockQuantity: 100
}) {
id
name
price
priceWithTax
priceCategory
}
}Updates an existing product identified by its id. All input fields are applied to the stored product. The updated product is returned so you can verify the change immediately without a separate query.
mutation {
updateProduct(
id: 1
input: {
name: "Gaming Laptop Pro"
description: "Professional gaming laptop with RTX 4080"
price: 1499.99
category: "Electronics"
stockQuantity: 25
}
) {
id
name
price
stockQuantity
}
}Removes the specified product from the catalog by ID. Returns true if the product was found and deleted successfully.
mutation {
deleteProduct(id: 6)
}GraphQL allows sending a mutation and a query together. The mutation runs first, then the query returns current catalog state. This pattern confirms the effect of the mutation in a single round-trip.
mutation {
createProduct(input: {
name: "USB Cable"
description: "USB-C to USB-C cable"
price: 12.99
category: "Accessories"
stockQuantity: 500
}) {
id
name
price
priceCategory
availabilityStatus
}
}
query {
productCount
}Variables decouple query logic from runtime values. This is the recommended pattern in client applications because it avoids string interpolation, improves type safety, and makes query documents reusable.
Declares the product ID as a typed variable ($productId: BigInteger!) in the operation signature. The variable value is supplied separately in the variables map, keeping the query document static.
|
Note
|
The id argument maps to a Java Long, which SmallRye GraphQL (the MicroProfile GraphQL runtime) exposes as the BigInteger scalar — not Int or ID. Using any other type causes a validation error.
|
query GetProduct($productId: BigInteger!) {
product(id: $productId) {
id
name
price
reviews {
rating
comment
}
}
}Variables:
{
"productId": 1
}Passes the entire product input as a typed variable ($input: ProductInput!). The mutation document stays constant while the payload changes at runtime, which also enables server-side validation of the input shape.
mutation CreateProduct($input: ProductInput!) {
createProduct(input: $input) {
id
name
price
}
}Variables:
{
"input": {
"name": "Smart Watch",
"description": "Fitness tracking smart watch",
"price": 199.99,
"category": "Wearables",
"stockQuantity": 75
}
}Aliases let you execute the same field multiple times with different arguments within a single request. Each result appears under its chosen alias key in the response, making it easy to fetch several data sets in one network call.
query {
budget: searchProducts(searchTerm: "mouse") {
id
name
price
}
premium: searchProducts(searchTerm: "laptop") {
id
name
price
}
}Fragments define reusable, named field selections that can be composed across multiple queries or mutations. They reduce repetition and keep large query documents maintainable. Fragments can spread other fragments using the … operator.
fragment ProductBasic on Product {
id
name
price
category
}
fragment ProductDetailed on Product {
...ProductBasic
description
stockQuantity
priceWithTax
availabilityStatus
}
query {
products {
...ProductBasic
}
product(id: 1) {
...ProductDetailed
reviews {
rating
comment
}
}
}The service uses ProductNotFoundException (extending RuntimeException) to signal missing products. MicroProfile GraphQL catches unchecked exceptions and returns a structured error response. The response message is controlled by the mp.graphql.defaultErrorMessage configuration property.
Requesting a product that does not exist triggers the custom exception. The product field resolves to null and the errors array describes the failure.
query {
product(id: 9999) {
id
name
}
}Expected response:
{
"data": {
"product": null
},
"errors": [
{
"message": "An error occurred processing your request",
"locations": [{"line": 2, "column": 3}],
"path": ["product"]
}
]
}All GraphQL requests are HTTP POST calls with a JSON body containing a query field. The examples below target the local development server.
Sends a compact inline query to retrieve all product IDs, names, and prices.
curl -X POST http://localhost:5060/graphql-catalog/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ products { id name price } }"}'Sends a parameterized query. The variables object maps variable names to their runtime values and is processed server-side alongside the query document.
curl -X POST http://localhost:5060/graphql-catalog/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query GetProduct($id: ID!) { product(id: $id) { id name price } }",
"variables": {"id": "1"}
}'Sends a createProduct mutation. Double quotes inside the query string are escaped with a backslash when using a single-quoted shell string.
curl -X POST http://localhost:5060/graphql-catalog/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "mutation { createProduct(input: { name: \"Test\", price: 9.99, category: \"Test\" }) { id name } }"
}'GraphQL supports built-in introspection queries that expose schema structure at runtime. Tools like GraphiQL use introspection to power autocompletion and inline documentation.
Returns all types in the schema — built-in scalars, custom types (Product, Review), and input types (ProductInput). Use this to understand the complete type inventory of the API.
query {
__schema {
types {
name
kind
}
}
}Returns the field definitions for the Product type: each field’s name and its return type. Useful for exploring the exact shape of a type without reading source code.
query {
__type(name: "Product") {
name
fields {
name
type {
name
kind
}
}
}
}catalog/
├── pom.xml
├── README.adoc
└── src/
└── main/
├── java/
│ └── io/microprofile/tutorial/graphql/product/
│ ├── Product.java # Product entity
│ ├── ProductInput.java # Input type for mutations
│ ├── ProductReview.java # Review entity
│ ├── ProductService.java # Business logic
│ ├── ReviewService.java # Review operations
│ ├── ProductGraphQLApi.java # GraphQL API
│ └── ProductNotFoundException.java # Custom exception
├── liberty/
│ └── config/
│ └── server.xml # Liberty configuration
└── resources/
└── META-INF/
└── microprofile-config.properties # Configuration
The ProductGraphQLApi class is marked with @GraphQLApi to expose it as a GraphQL endpoint:
@GraphQLApi
@ApplicationScoped
@Description("Product management GraphQL API")
public class ProductGraphQLApi {
// Queries and mutations
}Field resolvers add additional fields to types:
@Description("Reviews for this product")
public List<ProductReview> reviews(@Source Product product) {
return reviewService.findByProductId(product.getId());
}Computed fields are defined directly in the entity:
public Double getPriceWithTax() {
return price != null ? price * 1.08 : null;
}Custom exceptions extend RuntimeException. MicroProfile GraphQL catches unchecked exceptions and returns a structured error response with the message defined by mp.graphql.defaultErrorMessage:
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long productId) {
super("Product not found: " + productId);
}
}-
Use GraphiQL to interactively explore the schema and test queries
-
The schema is automatically generated from your Java classes
-
Use
@Descriptionannotations to document your API -
Use
@DefaultValuefor optional parameters -
Use
@Sourceto add computed or related fields to GraphQL types without modifying the entity class -
Use
@Nameto customize argument names in the schema