diff --git a/modules/ROOT/pages/chapter12/chapter12.adoc b/modules/ROOT/pages/chapter12/chapter12.adoc new file mode 100644 index 0000000..3dba539 --- /dev/null +++ b/modules/ROOT/pages/chapter12/chapter12.adoc @@ -0,0 +1,1303 @@ += MicroProfile GraphQL + +:imagesdir: ../../assets/images + +== Introduction + +APIs serve diverse clients including mobile applications,single-page web apps, and third-party services, each with different data requirements. REST APIs either return more data than the client needs (over-fetching) or too little, forcing multiple round trips to assemble a complete response (under-fetching). GraphQL eliminates both problems by shifting query control to the client, allowing callers to declare exactly the fields and relationships they need in a single request. + +This chapter explores MicroProfile GraphQL 2.0, a specification that brings GraphQL capabilities to Jakarta EE and MicroProfile applications. MicroProfile GraphQL complements REST by offering a strongly typed, schema-driven alternative that is particularly well suited to complex, relationship-rich domains. It uses an annotation-driven programming model, mapping Java classes and methods directly to GraphQL types, queries, and mutations with minimal boilerplate. + +This chapter extends the Catalog service to manage product reviews. You will implement the following features: + +* Queries that retrieve nested data in a single call +* Mutations that create and update resources +* Field resolvers that compute derived values on demand +* Error handling strategies that surface meaningful feedback to API consumers + +By the end of this chapter, you will understand how to build a MicroProfile GraphQL API, when GraphQL is the right choice, and how it differs from REST. + +== Topics Covered + +- What is GraphQL and key differences from REST +- Schema-first and Code-first approaches +- Project Dependencies (Maven and Gradle) +- Defining GraphQL service endpoints +- Annotations for defining GraphQL operations and metadata +- Scalar types, enumerations, and GraphQL interfaces +- Custom Object Mapping and Field Resolution +- Error Handling and Partial Results +- Integration with MicroProfile Config + +== What is GraphQL? + +GraphQL is a query language for APIs and a runtime for executing those queries against a type system you define for your data. It was developed by Facebook in 2012 and open-sourced in 2015. + +GraphQL is self-describing: the schema that powers an API also serves as its documentation, enabling powerful developer tooling and making APIs easier to evolve without versioning. + +A GraphQL service exposes a strongly typed schema that declares every type, field, and operation available. Clients construct queries that mirror the shape of the data they need, and the server responds with exactly that structure. This schema-first contract gives both client and server teams a shared, unambiguous definition of the API surface. + +== MicroProfile GraphQL 2.0 + +MicroProfile GraphQL 2.0 is the latest version of the GraphQL specification for Jakarta EE and MicroProfile applications. It builds on the foundation of GraphQL 15 and adds features that enhance developer productivity, schema flexibility, and runtime performance. + +=== Key Components of GraphQL + +The GraphQL consists of the following key components: + +* *Schema*: Defines the structure of your API, including types, queries, mutations, and subscriptions +* *Queries*: Read operations that fetch data from the server +* *Mutations*: Write operations that modify data on the server +* *Subscriptions*: Long-lived operations that push real-time updates from server to client +* *Types*: Define the shape of data objects in your API +* *Resolvers*: Functions that fetch or compute the value for a specific field in the schema +* *Introspection*: A built-in mechanism that allows clients to query the schema itself for documentation and tooling purposes + +=== Key Features of MicroProfile GraphQL 2.0 + +MicroProfile GraphQL 2.0 builds on Jakarta EE 10 to deliver the following capabilities for code-first GraphQL development: + +* *Code-First Development*: Define GraphQL schemas using Java annotations on POJOs and methods, eliminating the need to author or maintain a separate schema file +* *CDI Integration*: Seamless integration with Jakarta Contexts and Dependency Injection for lifecycle management and service injection +* *Type Safety*: Java's type system drives schema generation, catching structural errors at compile time rather than at runtime +* *Jakarta EE Alignment*: Built on Jakarta EE 10, including CDI 4.0, Bean Validation 3.0, and JSON-B 3.0 +* *Extensibility*: Support for custom scalar types, partial error results, and context propagation across resolver chains + +=== Schema-First Approach + +In a schema-first approach, you define your GraphQL schema using the GraphQL Schema Definition Language (SDL) before writing any implementation code. Resolvers are then written to match the agreed schema. This approach works well when: + +* Multiple teams need to agree on the API contract before implementation begins +* The API must be designed independently of any specific backend technology + +While MicroProfile GraphQL 2.0 primarily supports code-first development, you can manually create SDL files and ensure your Java implementation matches them. + +Although MicroProfile GraphQL 2.0 is oriented toward code-first development, you can adopt a schema-first workflow by authoring SDL files and verifying that your Java implementation matches them. The following example shows a minimal SDL definition for a product catalog: + +[source,graphql] +---- +type Product { + id: ID! + name: String! + price: Float! + description: String +} + +type Query { + products: [Product] + product(id: ID!): Product +} +---- + +=== Code-First Approach + +The code-first approach is the primary model of MicroProfile GraphQL 2.0. You define +your GraphQL API through annotated Java classes and methods, and the runtime +generates the schema automatically. This approach offers several advantages: + +* *Less Boilerplate*: No separate schema file to author or keep in sync with implementation +* *Refactoring Support*: IDE refactoring tools apply uniformly across the API definition and its implementation +* *Single Source of Truth*: Your annotated Java code defines both the schema structure and the resolver logic. + +== Key Differences Between REST and GraphQL + +Here are some of the key differences between REST and GraphQL: + +=== Data Fetching + +*REST*: Multiple endpoints expose fixed data structures, typically requiring separate +requests to gather related resources: + +[source] +---- +GET /api/users/123 +GET /api/users/123/orders +GET /api/users/123/preferences +---- + +*GraphQL*: Single endpoint with flexible queries. Clients retrieve all related +data in one request by specifying exactly the fields they need: + +[source, graphql] +---- +query { + user(id: 123) { + name + email + orders { + id + total + } + preferences { + theme + notifications + } + } +} +---- + +=== Versioning + +*REST*: API changes typically require new versioned endpoints (for example, `/api/v1/users` and `/api/v2/users`), which can result in maintaining multiple versions simultaneously. + +*GraphQL*: The schema evolves without breaking existing queries. New fields and types can be added freely, and deprecated fields can be marked and phased out gradually. + +=== Network Efficiency + +*REST*: Related resources often require multiple round trips to the server, increasing network overhead. + +*GraphQL*: All required data is retrieved in a single request, reducing latency and network overhead, a meaningful advantage on constrained mobile networks. + +=== Type Safety + +*REST*: Type safety depends on documentation and client-side validation. OpenAPI improves this but remains external to the protocol itself. + +*GraphQL*: Strong typing is intrinsic to the schema, enabling compile-time validation, auto-completion, and consistent tooling across client and server. + +=== Caching + +*REST*: HTTP caching is well-established and straightforward to implement at the transport level using standard cache headers. + +*GraphQL*: Caching is more complex due to the flexible nature of queries. It is typically implemented at the application level using techniques such as persisted queries or client-side tools such as +link:https://www.apollographql.com/docs/react/caching/overview[Apollo Client's InMemoryCache]. + +== Project Dependencies + +To use MicroProfile GraphQL in your project, you need to add the appropriate dependencies based on your build tool. + +=== Maven + +Add the following dependency to your `pom.xml`: + +[source, xml] +---- + + org.eclipse.microprofile.graphql + microprofile-graphql-api + 2.0 + provided + +---- + +The `provided` scope indicates that the implementation is provided by your MicroProfile runtime (such as Open Liberty, Quarkus, or Helidon). + +=== Gradle + +Add the following dependency to your `build.gradle`: + +[source, groovy] +---- +dependencies { + providedCompile 'org.eclipse.microprofile.graphql:microprofile-graphql-api:2.0' +} +---- + +== Defining GraphQL Service Endpoint + +MicroProfile GraphQL uses CDI beans marked with the `@GraphQLApi` annotation to expose GraphQL endpoints. All operations are automatically published under the `/graphql` endpoint. + +=== Marking CDI Bean as GraphQL Endpoint Using @GraphQLApi + +The `@GraphQLApi` annotation marks a CDI bean as a GraphQL endpoint, making its methods available as GraphQL operations: + +[source, java] +---- +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import jakarta.enterprise.context.ApplicationScoped; + +@GraphQLApi +@ApplicationScoped +public class ProductGraphQLApi { + + @Query("products") + public List getAllProducts() { + // Implementation + return productService.findAll(); + } +} +---- + +Key points about `@GraphQLApi`: + +* It should be placed on a CDI bean (typically `@ApplicationScoped` or `@RequestScoped`) +* All public methods become potential GraphQL operations +* Methods must be annotated with `@Query` or `@Mutation` to be exposed +* The bean can inject other CDI beans and services + +=== GraphQL Entities and API Class + +GraphQL entities are typically represented as POJOs (Plain Old Java Objects). The structure of these POJOs defines the GraphQL types in your schema: + +[source, java] +---- +//.. + +import org.eclipse.microprofile.graphql.Type; + +@Type("Product") +public class Product { + private Long id; + private String name; + private String description; + private Double price; + private String category; + + // Constructors + public Product() {} + + public Product(Long id, String name, Double price) { + this.id = id; + this.name = name; + this.price = price; + } + + // Getters and Setters + // ... + +} +---- + +A complete API class with entity management: + +[source, java] +---- +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.util.List; + +@GraphQLApi +@ApplicationScoped +public class ProductGraphQLApi { + + @Inject + ProductService productService; + + @Query("products") + public List getAllProducts() { + return productService.findAll(); + } + + @Query("product") + public Product getProduct(@Name("id") Long id) { + return productService.findById(id); + } + + @Mutation("createProduct") + public Product createProduct(@Name("input") ProductInput input) { + return productService.create(input); + } + + @Mutation("updateProduct") + public Product updateProduct(@Name("id") Long id, + @Name("input") ProductInput input) { + return productService.update(id, input); + } +} +---- + +=== Publishing APIs Under /graphql Endpoint + +All GraphQL operations are automatically published under a single `/graphql` endpoint by the MicroProfile GraphQL implementation. Clients interact with this endpoint using HTTP POST requests containing GraphQL queries. + +Example GraphQL query request: + +[source, json] +---- +POST /graphql +Content-Type: application/json + +{ + "query": "{ products { id name price } }" +} +---- + +Response: + +[source, json] +---- +{ + "data": { + "products": [ + { + "id": "1", + "name": "Laptop", + "price": 999.99 + }, + { + "id": "2", + "name": "Mouse", + "price": 29.99 + } + ] + } +} +---- + +Most MicroProfile implementations also provide a GraphiQL or GraphQL Playground interface for interactive exploration, typically accessible at `/graphql-ui` or `/graphiql`. + +== Annotations for Defining GraphQL Operations and Adding Metadata + +MicroProfile GraphQL provides a rich set of annotations to define operations and add metadata to your GraphQL schema. + +=== Defining Read Operations with @Query + +The `@Query` annotation marks a method as a GraphQL query operation (read operation): + +[source, java] +---- +import org.eclipse.microprofile.graphql.Query; + +@GraphQLApi +public class ProductGraphQLApi { + + @Query("allProducts") + public List getAllProducts() { + return productService.findAll(); + } + + @Query("product") + public Product getProductById(@Name("id") Long id) { + return productService.findById(id); + } + + @Query("searchProducts") + public List searchProducts(@Name("searchTerm") String searchTerm, + @Name("category") String category) { + return productService.search(searchTerm, category); + } +} +---- + +=== Defining Write Operations with @Mutation + +The `@Mutation` annotation marks a method as a GraphQL mutation operation (write operation): + +[source, java] +---- +import org.eclipse.microprofile.graphql.Mutation; + +@GraphQLApi +public class ProductGraphQLApi { + + @Mutation("createProduct") + public Product createProduct(@Name("input") ProductInput input) { + return productService.create(input); + } + + @Mutation("deleteProduct") + public boolean deleteProduct(@Name("id") Long id) { + return productService.delete(id); + } +} +---- + +=== Query Naming and Descriptions - @Name, @Description, @DefaultValue + +Use `@Name` to specify the name of a field or parameter in the GraphQL schema: + +[source, java] +---- +import org.eclipse.microprofile.graphql.Name; + +@GraphQLApi +public class ProductGraphQLApi { + + @Query("product") + public Product getProduct(@Name("productId") Long id) { + return productService.findById(id); + } +} +---- + +Use `@Description` to add documentation to your schema: + +[source, java] +---- +import org.eclipse.microprofile.graphql.Description; + +@GraphQLApi +@Description("Product management API") +public class ProductGraphQLApi { + + @Query("products") + @Description("Retrieves all products from the catalog") + public List getAllProducts() { + return productService.findAll(); + } + + @Query("product") + @Description("Retrieves a single product by its unique identifier") + public Product getProduct( + @Name("id") + @Description("The unique identifier of the product") + Long id) { + return productService.findById(id); + } +} +---- + +Use `@DefaultValue` to specify default values for parameters: + +[source, java] +---- +import org.eclipse.microprofile.graphql.DefaultValue; + +@GraphQLApi +public class ProductGraphQLApi { + + @Query("products") + public List getProducts( + @Name("limit") + @DefaultValue("10") + int limit, + @Name("offset") + @DefaultValue("0") + int offset, + @Name("sortBy") + @DefaultValue("name") + String sortBy) { + return productService.findAll(limit, offset, sortBy); + } +} +---- + +Client query with defaults: + +[source, graphql] +---- +# Uses all default values +query { + products { + id + name + } +} + +# Overrides limit +query { + products(limit: 20) { + id + name + } +} +---- + +=== Returning POJOs, Primitives, Collections and Type Safety with POJOs + +MicroProfile GraphQL supports returning various types: + +*Primitives and Wrappers*: + +[source, java] +---- +@Query("productCount") +public int getProductCount() { + return productService.count(); +} + +@Query("averagePrice") +public Double getAveragePrice() { + return productService.calculateAveragePrice(); +} +---- + +*Collections*: + +[source, java] +---- +@Query("products") +public List getProducts() { + return productService.findAll(); +} + +@Query("productIds") +public Set getProductIds() { + return productService.getAllIds(); +} +---- + +*POJOs with Type Safety*: + +[source, java] +---- +@Type("Product") +@Description("A product in the catalog") +public class Product { + + @Description("Unique product identifier") + private Long id; + + @Description("Product name") + @NonNull + private String name; + + @Description("Product price in USD") + private Double price; + + @Description("Optional product description") + private String description; + + // Getters and setters +} +---- + +The `@NonNull` annotation indicates that a field cannot be null, which translates to the `!` operator in GraphQL schema: + +[source, graphql] +---- +type Product { + id: ID + name: String! + price: Float + description: String +} +---- + +*Complex Nested Types*: + +[source, java] +---- +@Type("Order") +public class Order { + private Long id; + private Customer customer; + private List items; + private OrderStatus status; + + // Getters and setters +} + +@Type("OrderItem") +public class OrderItem { + private Product product; + private Integer quantity; + private Double subtotal; + + // Getters and setters +} +---- + +=== Defining GraphQL Interfaces with @Interface + +Use the `@Interface` annotation on a Java interface to define a GraphQL interface type. The MicroProfile GraphQL implementation generates a GraphQL interface in the schema for every class in the application that implements the annotated Java interface: + +[source, java] +---- +import org.eclipse.microprofile.graphql.Interface; +import org.eclipse.microprofile.graphql.Description; + +@Interface +public interface Identifiable { + Long getId(); +} + +public class Product implements Identifiable { + private Long id; + private String name; + + @Override + @Description("Unique product identifier") + public Long getId() { return id; } + + // ... +} + +public class ProductReview implements Identifiable { + private Long id; + private String comment; + + @Override + @Description("Unique review identifier") + public Long getId() { return id; } + + // ... +} +---- + +This generates a schema such as: + +[source, graphql] +---- +interface Identifiable { + id: BigInteger +} + +type Product implements Identifiable { + "Unique product identifier" + id: BigInteger + # ... +} + +type Review implements Identifiable { + "Unique review identifier" + id: BigInteger + # ... +} +---- + +NOTE: The MicroProfile GraphQL 2.0 specification does not support `@Interface` on input types. + +=== Defining Enumerable Types with @Enum + +Use the `@Enum` annotation to name an enum type in the generated GraphQL schema. When neither `@Enum` nor `@Name` is present, the implementation uses the Java enum name: + +[source, java] +---- +import org.eclipse.microprofile.graphql.Enum; +import org.eclipse.microprofile.graphql.Type; + +@Type("Product") +public class Product { + private String name; + private StockStatus stockStatus; + + @Enum("StockStatus") + public enum StockStatus { + IN_STOCK, LOW_STOCK, OUT_OF_STOCK + } + + // getters and setters +} +---- + +This generates the following in the schema: + +[source, graphql] +---- +enum StockStatus { + IN_STOCK + LOW_STOCK + OUT_OF_STOCK +} + +type Product { + name: String + stockStatus: StockStatus + # ... +} +---- + +The GraphQL runtime returns a validation error when the client sends a value that is not included in the enumerated type. + +=== Formatting Scalars with @NumberFormat and @DateFormat + +Use the `@NumberFormat` annotation to apply a pattern to number scalar fields. When a format is applied, the formatted result is of type `String` in the GraphQL response: + +[source, java] +---- +import org.eclipse.microprofile.graphql.NumberFormat; +import org.eclipse.microprofile.graphql.DateFormat; +import org.eclipse.microprofile.graphql.Type; + +@Type("Product") +public class Product { + + @NumberFormat(value = "$ #,##0.00", locale = "en-US") + private Double price; + + // getters and setters +} +---- + +Similarly, use `@DateFormat` to control how date scalar fields are serialized: + +[source, java] +---- +import org.eclipse.microprofile.graphql.DateFormat; +import java.time.LocalDate; + +@Type("Product") +public class Product { + + @DateFormat(value = "dd MMM yyyy") + private LocalDate releaseDate; + + // getters and setters +} +---- + +Date scalars default to ISO-8601 format when no `@DateFormat` annotation is present. The GraphQL annotation takes precedence over the equivalent JSON-B annotation (`@JsonbNumberFormat` or `@JsonbDateFormat`) when both are present on the same field. + +You may also place `@NumberFormat` or `@DateFormat` on a `@Query` or `@Mutation` method to transform the return value: + +[source, java] +---- +@Query +@DateFormat(value = "dd MMM yyyy") +public LocalDate getProductReleaseDate(@Name("id") Long id) { + return productService.findById(id).getReleaseDate(); +} +---- + +=== Excluding Fields with @Ignore + +Use the `@Ignore` annotation to prevent a field from appearing in the generated GraphQL schema. The placement of the annotation determines which schema section is affected: + +* Placed on the Java field: excludes the field from both the output type and the input type. +* Placed on the getter method: excludes the field from the output type only. +* Placed on the setter method: excludes the field from the input type only. + +[source, java] +---- +import org.eclipse.microprofile.graphql.Ignore; +import org.eclipse.microprofile.graphql.Type; + +@Type("Product") +public class Product { + + @Ignore + private String internalCode; // excluded from both type and input + + private String name; + + @Ignore + public String getAuditLog() { // excluded from output type only + return auditLog; + } + + // getters and setters +} +---- + +== Custom Object Mapping and Field Resolution + +MicroProfile GraphQL provides powerful features for custom field resolution and handling complex data relationships. + +=== Using @Source to Resolve Nested Types + +The `@Source` annotation allows you to add fields to a type by creating resolver methods. This is particularly useful for fields that require additional processing or data fetching: + +[source, java] +---- +@GraphQLApi +public class ProductGraphQLApi { + + @Inject + ReviewService reviewService; + + @Inject + InventoryService inventoryService; + + // Regular query + @Query("products") + public List getAllProducts() { + return productService.findAll(); + } + + // Field resolver - adds 'reviews' field to Product type + public List reviews(@Source Product product) { + return reviewService.findByProductId(product.getId()); + } + + // Field resolver - adds 'stockLevel' field to Product type + public int stockLevel(@Source Product product) { + return inventoryService.getStockLevel(product.getId()); + } + + // Field resolver with parameters + public List topReviews(@Source Product product, + @Name("limit") @DefaultValue("5") int limit) { + return reviewService.findTopReviewsByProductId(product.getId(), limit); + } +} +---- + +This allows clients to query: + +[source, graphql] +---- +{ + products { + id + name + price + reviews { rating comment } + topReviews(limit: 2) { rating comment } + averageRating + priceCategory + } +} +---- + +=== Derived/Computed Fields + +You can add computed fields directly in your entity classes or through resolver methods: + +*In Entity Class*: + +[source, java] +---- +@Type("Product") +public class Product { + private Long id; + private String name; + private Double price; + private Double taxRate; + + // Regular getters... + + // Computed field + public Double getPriceWithTax() { + return price * (1 + taxRate); + } + + // Computed field with logic + public String getDisplayName() { + return name != null ? name.toUpperCase() : "UNKNOWN"; + } +} +---- + +*Using Resolver Methods*: + +[source, java] +---- +@GraphQLApi +public class ProductGraphQLApi { + + @Inject + PricingService pricingService; + + // Computed field using external service + public Double discountedPrice(@Source Product product, + @Name("discountCode") String discountCode) { + return pricingService.calculateDiscountedPrice( + product.getPrice(), discountCode); + } + + // Complex computation + public String priceCategory(@Source Product product) { + Double price = product.getPrice(); + if (price < 50) return "BUDGET"; + if (price < 200) return "STANDARD"; + if (price < 500) return "PREMIUM"; + return "LUXURY"; + } +} +---- + +== Error Handling and Partial Results + +GraphQL provides sophisticated error handling capabilities that allow returning partial results even when errors occur. + +=== Client Errors + +Client errors occur when the client sends invalid queries or arguments. These are automatically handled by the GraphQL runtime: + +* *Syntax Errors*: Invalid GraphQL query syntax +* *Validation Errors*: Query does not match the schema +* *Variable Errors*: Invalid or missing variables + +Example client error response: + +[source, json] +---- +{ + "errors": [ + { + "message": "Validation error of type FieldUndefined: Field 'invalidField' is undefined @ 'product/invalidField'", + "locations": [ + { + "line": 2, + "column": 5 + } + ], + "extensions": { + "classification": "ValidationError" + } + } + ] +} +---- + +=== Server Errors (Unchecked and Checked Exceptions) + +*Unchecked Exceptions*: + +Runtime exceptions are caught and converted to GraphQL errors: + +[source, java] +---- +@GraphQLApi +public class ProductGraphQLApi { + + @Query("product") + public Product getProduct(@Name("id") Long id) { + Product product = productService.findById(id); + if (product == null) { + throw new RuntimeException("Product not found: " + id); + } + return product; + } +} +---- + +Response with error: + +[source, json] +---- +{ + "data": { + "product": null + }, + "errors": [ + { + "message": "Product not found: 123", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": ["product"], + "extensions": { + "exception": { + "className": "java.lang.RuntimeException" + } + } + } + ] +} +---- + +*Checked Exceptions*: + +You can throw checked exceptions and handle them appropriately: + +[source, java] +---- +public class ProductNotFoundException extends Exception { + public ProductNotFoundException(Long id) { + super("Product not found: " + id); + } +} + +@GraphQLApi +public class ProductGraphQLApi { + + @Query("product") + public Product getProduct(@Name("id") Long id) + throws ProductNotFoundException { + Product product = productService.findById(id); + if (product == null) { + throw new ProductNotFoundException(id); + } + return product; + } +} +---- + +=== Custom Error Handling with GraphQLException + +Create custom exception handlers for better error messages and control. + +NOTE: The MicroProfile GraphQL 2.0 API's `GraphQLException` class does not provide a mutable `getExtensions()` method. To include detailed error information, use descriptive error messages in the constructor and store additional data as instance fields. + +[source, java] +---- +import org.eclipse.microprofile.graphql.GraphQLException; + +// Custom exception +public class ProductNotFoundException extends GraphQLException { + private final Long productId; + + public ProductNotFoundException(Long productId) { + super(String.format("Product not found: %d", productId), + GraphQLException.ExceptionType.DataFetchingException); + this.productId = productId; + } + + public Long getProductId() { return productId; } +} + +// Custom exception with more details +public class InsufficientStockException extends GraphQLException { + private final Long productId; + private final int requestedQuantity; + private final int availableQuantity; + + public InsufficientStockException(Long productId, int requested, int available) { + super(String.format("Insufficient stock for product %d: requested %d, available %d", + productId, requested, available), + GraphQLException.ExceptionType.DataFetchingException); + this.productId = productId; + this.requestedQuantity = requested; + this.availableQuantity = available; + } + + public Long getProductId() { return productId; } + public int getRequestedQuantity() { return requestedQuantity; } + public int getAvailableQuantity() { return availableQuantity; } +} +---- + +Using custom exceptions: + +[source, java] +---- +@GraphQLApi +public class ProductGraphQLApi { + + @Query("product") + public Product getProduct(@Name("id") Long id) + throws ProductNotFoundException { + Product product = productService.findById(id); + if (product == null) { + throw new ProductNotFoundException(id); + } + return product; + } + + @Mutation("orderProduct") + public Order orderProduct(@Name("productId") Long productId, + @Name("quantity") int quantity) + throws InsufficientStockException { + int available = inventoryService.getStockLevel(productId); + if (available < quantity) { + throw new InsufficientStockException(productId, quantity, available); + } + return orderService.createOrder(productId, quantity); + } +} +---- + +Custom error response: + +[source, json] +---- +{ + "data": { + "orderProduct": null + }, + "errors": [ + { + "message": "Insufficient stock for product 123: requested 10, available 5", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": ["orderProduct"], + "extensions": { + "exception": "io.microprofile.tutorial.graphql.product.exception.InsufficientStockException", + "classification": "DataFetchingException", + "code": "insufficient-stock" + } + } + ] +} +---- + +=== Partial Results + +GraphQL can return partial results when some fields fail but others succeed: + +[source, java] +---- +@GraphQLApi +public class ProductGraphQLApi { + + @Query("products") + public List getProducts() { + return productService.findAll(); // Success + } + + // This field might fail for individual products + public Double specialPrice(@Source Product product) + throws GraphQLException { + try { + return pricingService.calculateSpecialPrice(product.getId()); + } catch (Exception e) { + throw new GraphQLException("Failed to calculate special price", + GraphQLException.ExceptionType.DataFetchingException); + } + } +} +---- + +Query: + +[source, graphql] +---- +query { + products { + id + name + price + specialPrice + } +} +---- + +Partial result response: + +[source, json] +---- +{ + "data": { + "products": [ + { + "id": "1", + "name": "Laptop", + "price": 999.99, + "specialPrice": 899.99 + }, + { + "id": "2", + "name": "Mouse", + "price": 29.99, + "specialPrice": null + } + ] + }, + "errors": [ + { + "message": "Failed to calculate special price", + "path": ["products", 1, "specialPrice"] + } + ] +} +---- +== GraphQL UI (GraphiQL) + +Most MicroProfile GraphQL implementations provide an interactive GraphQL UI for exploring and testing your API. + +=== Enabling GraphQL UI on Open Liberty + +Add the following variable to your Liberty `server.xml` configuration: + +[source, xml] +---- + +---- + +The GraphQL UI is then accessible at: + +---- +http://localhost://graphql-ui +---- + +For example: + +---- +http://localhost:5060/graphql-catalog/graphql-ui +---- + +=== GraphQL UI Features + +* *Interactive Query Editor*: Write and execute queries with syntax highlighting +* *Schema Explorer*: Browse the complete schema with documentation +* *Auto-Completion*: Get field and argument suggestions as you type +* *Query History*: Access previously executed queries +* *Variable Editor*: Test queries with variables +* *Documentation Panel*: View inline documentation from `@Description` annotations + +=== Using GraphQL UI for Development + +The GraphQL UI is invaluable during development: + +1. Explore the auto-generated schema documentation +2. Test queries and mutations interactively +3. Validate field names and argument types +4. Experiment with fragments, aliases, and variables +5. Debug error responses + +NOTE: GraphQL UI should typically be disabled in production environments. Set `io.openliberty.enableGraphQLUI` to `false` or remove the variable entirely. + +== Integration with MicroProfile Config + +MicroProfile GraphQL integrates with MicroProfile Config, allowing you to externalize configuration for your GraphQL services. This is particularly useful for controlling error message visibility across environmentsfor example, exposing detailed messages in development while suppressing them in production. + +=== Configuring Error Message Visibility + +The following properties control which exception messages are returned to clients: + +[source,properties] +---- +mp.graphql.defaultErrorMessage=An error occurred processing your request +mp.graphql.hideErrorMessage=java.lang.NullPointerException +mp.graphql.showErrorMessage=io.microprofile.tutorial.graphql.product.exception.ProductNotFoundException +---- + +- `mp.graphql.defaultErrorMessage` defines the generic message returned to clients whenever an exception message is suppressed. +- `mp.graphql.hideErrorMessage` accepts a comma-separated list of fully qualified exception class names whose messages are replaced with the default error message. By default, all unchecked exceptions (subclasses of `RuntimeException`) have their messages hidden. +- `mp.graphql.showErrorMessage` accepts a comma-separated list of exception class names whose messages are forwarded to clients as-is. By default, all checked exceptions have their messages shown. + +=== Using Config in GraphQL Services + +Inject configuration values into your GraphQL API classes: + +[source, java] +---- +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; + +import jakarta.inject.Inject; + +@GraphQLApi +public class ProductGraphQLApi { + + @Inject + @ConfigProperty(name = "product.max.results", defaultValue = "100") + private Integer maxResults; + + @Inject + @ConfigProperty(name = "product.currency", defaultValue = "USD") + private String currency; + + @Query("products") + public List getAllProducts() { + // Use maxResults to limit query results + return productService.getProducts(maxResults); + } +} +---- + +=== Dynamic Configuration + +You can also use dynamic configuration to adjust behavior at runtime: + +[source, java] +---- +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +@GraphQLApi +public class ConfigurableApi { + + @Query + public String getConfigValue(@Name("key") String key) { + Config config = ConfigProvider.getConfig(); + return config.getOptionalValue(key, String.class) + .orElse("Not configured"); + } +} +---- + +== Summary + +In this chapter, you explored MicroProfile GraphQL 2.0, a specification for building flexible and efficient GraphQL APIs in Jakarta EE and MicroProfile environments. The chapter covered: + +* The fundamentals of GraphQL and how it differs from REST +* Setting up MicroProfile GraphQL in your project with Maven or Gradle +* Defining GraphQL endpoints using `@GraphQLApi` and exposing operations through the `/graphql` endpoint +* Using annotations like `@Query`, `@Mutation`, `@Name`, `@Description`, and `@DefaultValue` to define and document your API +* Defining GraphQL interface types with `@Interface`, enumerable types with `@Enum`, and excluding fields with `@Ignore` +* Formatting scalar values with `@NumberFormat` and `@DateFormat` +* Implementing custom field resolvers with `@Source` to handle complex data relationships +* Handling errors with `GraphQLException` and returning partial results +* Controlling error message visibility with `mp.graphql.showErrorMessage` and `mp.graphql.hideErrorMessage` +* Using GraphQL UI (GraphiQL) for interactive API development and testing +* Integrating with MicroProfile Config for externalized configuration + +MicroProfile GraphQL 2.0 provides a strong foundation for building modern APIs that give clients the flexibility to request exactly the data they need while maintaining strong typing and a good developer experience. + +For detailed information about MicroProfile GraphQL 2.0, refer to the official specification at https://download.eclipse.org/microprofile/microprofile-graphql-2.0/microprofile-graphql-spec-2.0.html[MicroProfile GraphQL 2.0 Specification].