EzyHTTP fetcher implementation guide
Updated at 1783958059000EzyHTTP GraphQL fetchers are the application-facing part of the GraphQL module. The runtime parses the query and dispatches it; the fetcher decides what data to load, how to validate arguments, which service method to call, and what DTO shape to return.
This guide assumes the GraphQL module is already enabled and focuses only on fetcher implementation. For the full request lifecycle, query parsing, dispatch, and response envelope, see the separate “EzyHTTP how GraphQL works” article.
Installation flow
Use this flow when adding a new GraphQL fetcher to an EzyHTTP application:
flowchart TD
A[Enable GraphQL module] --> B[Create fetcher class]
B --> C[Implement getData]
C --> D[Declare query name]
D --> E{Registration style}
E --> F[Register as Ezy singleton]
E --> G[Return from fetcher provider]
F --> H[Add metadata if needed]
G --> H
H --> I[Add interceptor rules if needed]
I --> J[Call query and verify response shape]
The usual path is simple: enable GraphQL, create a singleton fetcher, give it a query name, implement
getData, and test it with a query that selects only the fields the client needs. Use a fetcher provider only when fetchers are dynamic or plugin-driven.Where a fetcher begins
A fetcher is responsible for one top-level query field. If the client sends:
{
profile(userId: 10) {
id
displayName
}
}
EzyHTTP looks for the fetcher named
profile. Once the fetcher is called, the request has already been parsed into a GraphQLQueryDefinition. That means fetcher code should not parse raw GraphQL strings, inspect HTTP methods, or build the final GraphQL response envelope. It should work at the application boundary:request context + parsed query -> application service call -> response DTO
The fetcher contract
A fetcher implements one method:
Object getData(
RequestArguments arguments,
GraphQLQueryDefinition query
);
RequestArguments is the current EzyHTTP request context. It contains values prepared by the HTTP layer and interceptors, so a fetcher can reuse the same request-scoped data model as regular controllers.GraphQLQueryDefinition is the parsed top-level query. It contains:| Data | Example |
|---|---|
| Query name | profile |
| Query arguments | profile(userId: 10) |
| Selected fields | id, displayName, friends |
| Nested field arguments | friends(limit: 5) |
The simplest implementation extends
GraphQLAbstractDataFetcher:@EzySingleton public class ProfileDataFetcher extends GraphQLAbstractDataFetcher { private final ProfileService profileService; public ProfileDataFetcher(ProfileService profileService) { this.profileService = profileService; } @Override public ProfileResponse getData( RequestArguments arguments, GraphQLQueryDefinition query ) { Long userId = query.getArgumentValue("userId", Long.class); return profileService.getProfile(userId); } @Override public String getQueryName() { return "profile"; } }
The important boundary is the method body: read what you need from
query and arguments, call your service layer, and return a response object. Keep transport concerns outside the fetcher.Naming a fetcher
EzyHTTP resolves the fetcher name in one of two ways.
The first option is to override
getQueryName():@Override public String getQueryName() { return "profile"; }
The second option is to annotate the fetcher:
@GraphQLQuery("profile") @EzySingleton public class ProfileDataFetcher extends GraphQLAbstractDataFetcher { // ... }
@GraphQLQuery supports both value and name; name takes precedence when both are present. It also supports group, which is useful for organizing queries:@GraphQLQuery(name = "profile", group = "user") @EzySingleton public class ProfileDataFetcher extends GraphQLAbstractDataFetcher { // ... }
If neither
getQueryName() nor @GraphQLQuery provides a name, the default implementation throws an error during registration or use. In practice, every fetcher should declare its query name explicitly.Registering fetchers
For normal application fetchers, register them as Ezy singletons:
@EzySingleton public class ProfileDataFetcher extends GraphQLAbstractDataFetcher { // ... }
When GraphQL is enabled, EzyHTTP discovers these singletons and adds them to the fetcher manager. The manager stores the mapping:
query name -> fetcher instance
It also stores optional metadata:
| Metadata | Source |
|---|---|
| Query group | getQueryGroupName() or @GraphQLQuery(group = "...") |
| Authenticated query | @Authenticated or AuthenticatedController |
| Management query | @EzyManagement, ManagementController, or ManageableController |
| Payment query | @EzyPayment or PaymentController |
If a fetcher does not declare a group, EzyHTTP puts it in the
default group.Dynamic fetchers
Use
GraphQLDataFetcherProvider when the fetcher cannot or should not be registered eagerly.@EzySingleton public class DynamicFetcherProvider implements GraphQLDataFetcherProvider { private final ProfileService profileService; public DynamicFetcherProvider(ProfileService profileService) { this.profileService = profileService; } @Override public GraphQLDataFetcher provide(String queryName) { if ("profile".equals(queryName)) { return new ProfileDataFetcher(profileService); } return null; } }
The fetcher manager checks registered fetchers first. If no fetcher is found for the query name, it asks each provider. A provider should return
null when it does not own the query.Prefer regular singleton fetchers for most APIs. Providers are best for plugin-style modules, tenant-specific query sets, or generated fetchers.
Reading query arguments
Use
getArgumentValue to read arguments on the top-level query:Long userId = query.getArgumentValue("userId", Long.class); String locale = query.getArgumentValue("locale", String.class); Integer limit = query.getArgumentValue("limit", Integer.class);
The typed overload converts strings and mapped values to the requested Java type where possible. If the argument is absent, it returns
null.Example query:
{
profile(userId: 10, locale: "en") {
id
displayName
}
}
For optional arguments, set application-level defaults in the fetcher:
Integer limit = query.getArgumentValue("limit", Integer.class); if (limit == null) { limit = 20; }
Keep validation close to the fetcher or service boundary. The GraphQL layer parses and converts arguments, but your fetcher still owns business constraints such as maximum page size, required identifiers, and allowed enum values.
Validating arguments
Treat parsed arguments as input, not as trusted business data. A practical fetcher usually validates three things before calling the service layer:
| Check | Example |
|---|---|
| Required value | userId must not be null |
| Range | limit must be between 1 and 100 |
| Relationship | The current user can read the requested profile |
For simple validation, fail early:
Long userId = query.getArgumentValue("userId", Long.class); if (userId == null) { throw GraphQLFetcherException.builder() .error( GraphQLError.builder() .message("userId is required") .path("profile", "userId") .build() ) .build(); }
For business validation, delegate to the service layer and translate known domain failures into client-facing GraphQL errors only when the client can act on them. Unexpected exceptions should still be logged and handled by the normal server error flow.
Reading nested field arguments
Nested selected fields can have their own arguments:
{
profile(userId: 10) {
id
posts(limit: 5) {
title
createdAt
}
}
}
The fetcher can read them through the query definition:
Integer postLimit = query.getFieldArgumentValue(
"limit",
Integer.class,
"posts"
);
This is useful when one top-level fetcher owns a data graph but wants client-provided options for nested parts. It also lets the fetcher avoid unnecessary work:
boolean includePosts = query.getField("posts") != null;
If
posts is not selected, the fetcher can skip loading posts entirely.Response shape rules
A fetcher should return an object that can be converted to a map by EzyHTTP's object mapper. DTOs, JavaBeans, Lombok-backed response objects, and maps are good fits.
Design those DTOs around the GraphQL fields you want clients to request:
@Getter @Builder public class ProfileResponse { private final long id; private final String displayName; private final String avatarUrl; private final List<PostResponse> posts; }
Avoid returning a DTO whose property names are implementation-oriented, such as database column names or internal aggregate names, unless those names are meant to be part of the public GraphQL contract.
For nested selections, nested values must also be map-like after conversion. Lists should contain objects or maps, not scalar values, when the query selects subfields. This is a good match:
{
profile(userId: 10) {
posts {
title
}
}
}
return ProfileResponse.builder() .id(10L) .posts(List.of(new PostResponse("GraphQL in EzyHTTP"))) .build();
But if
posts is a List<String> and the query asks for posts { title }, the response shape is invalid because EzyHTTP expects each list item to be map-like for nested filtering.When you need to expose a computed field, compute it in the DTO or assembler layer before returning:
return ProfileResponse.builder() .id(user.getId()) .displayName(user.getFirstName() + " " + user.getLastName()) .avatarUrl(imageUrlFactory.create(user.getAvatarPath())) .build();
For internal tools, EzyHTTP also supports the special
* selection to include all non-null fields from a map-like object. Public APIs should normally prefer explicit fields, because it makes the contract easier to evolve.Handling request context
Because fetchers receive
RequestArguments, they can reuse request-scoped values prepared by EzyHTTP interceptors.
Long currentUserId = arguments.getArgument("currentUserId");
This is a good pattern for authenticated GraphQL APIs:
@Authenticated @GraphQLQuery(name = "viewer", group = "user") @EzySingleton public class ViewerDataFetcher extends GraphQLAbstractDataFetcher { @Override public ViewerResponse getData( RequestArguments arguments, GraphQLQueryDefinition query ) { Long userId = arguments.getArgument("currentUserId"); return userService.getViewer(userId); } }
The fetcher does not need to parse tokens or read headers directly. That work belongs in normal request infrastructure or GraphQL interceptors.
Keeping cross-cutting code out of fetchers
Fetchers should stay focused on data loading. Shared concerns such as access checks, query logging, request enrichment, and feature gating are better placed in
GraphQLInterceptor.@EzySingleton public class QueryAccessInterceptor implements GraphQLInterceptor { @Override public boolean preHandle( RequestArguments arguments, String queryGroup, String queryName, GraphQLQueryDefinition queryDefinition, GraphQLDataFetcher dataFetcher ) { // return false to reject this query return true; } }
Interceptors are sorted by
getPriority() in ascending order. Returning false from preHandle rejects the current query. Use that for shared decisions; use fetcher code for query-specific validation and service calls.This keeps fetchers easy to test. A fetcher test can provide a prepared
RequestArguments object and a parsed query definition, then assert that the correct service call is made.Reporting errors
If no fetcher is found for a query name, EzyHTTP returns a GraphQL-style error response. Fetchers can also throw
GraphQLFetcherException when they need to return structured GraphQL errors:throw GraphQLFetcherException.builder() .error( GraphQLError.builder() .message("profile not found") .build() ) .build();
Use this for client-facing GraphQL errors. For unexpected internal failures, it is usually better to let the normal exception flow handle logging and response mapping, unless you need a specific GraphQL error payload.
Implementation checklist
Use this checklist when adding a new fetcher:
| Check | Why it matters |
|---|---|
| Declare a query name | The dispatcher needs a stable top-level GraphQL field name |
| Register as singleton | The runtime discovers fetchers from the application container |
| Return DTOs or maps | The response is converted to a map before field filtering |
| Validate arguments | Parsing is not the same as business validation |
| Inspect selected fields when useful | Avoid loading expensive nested data that was not requested |
| Keep auth outside data loading | Use request infrastructure and interceptors for cross-cutting rules |
| Throw structured errors deliberately | Client-facing GraphQL errors should be predictable |
Common mistakes
Missing query name is the most basic mistake. If a fetcher has neither
getQueryName() nor @GraphQLQuery, EzyHTTP cannot dispatch to it.Returning scalar data for a nested selection is another common issue. A scalar can work for a simple value-style query, but nested field filtering expects map-like data.
Loading every relationship unconditionally can make a fetcher expensive. The query definition tells you which fields were requested, so use it when a nested field requires extra database or network calls.
Putting authentication logic directly in every fetcher also tends to become repetitive. Prefer regular request authentication,
@Authenticated metadata, and GraphQLInterceptor for shared access decisions.Conclusion
An EzyHTTP GraphQL fetcher should be small, named clearly, registered as a singleton, and focused on one top-level query. It reads typed arguments from
GraphQLQueryDefinition, uses RequestArguments for request-scoped context, returns DTOs or maps, and lets EzyHTTP filter the response to the client's selected fields.That design keeps GraphQL support lightweight: fetchers stay close to application services, while the framework handles parsing, dispatch, interception, response shaping, and GraphQL-style error payloads.