EzyHTTP how GraphQL works
Updated at 1783872859000EzyHTTP includes a lightweight GraphQL module for applications that want GraphQL-style field selection without moving the whole HTTP stack away from EzyHTTP controllers, interceptors, request arguments, and object mapping.
The module exposes a single
/graphql endpoint. A client sends a GraphQL query, EzyHTTP parses the query into an internal tree, finds a matching data fetcher by query name, executes that fetcher, then filters the returned data so the response contains only the fields requested by the client.
flowchart LR
A[Client request] --> B[graphql endpoint]
B --> C[Parse query]
C --> D[Find data fetcher]
D --> E[Run interceptors]
E --> F[Fetch data]
F --> G[Convert to map]
G --> H[Filter requested fields]
H --> I[Return data or errors]
When to Use It
EzyHTTP GraphQL is useful when a REST-style server already owns the business logic, but clients need more control over response shape. Instead of creating many REST endpoints for slightly different projections, one fetcher can return a complete object and let the GraphQL layer select fields from it.
It is intentionally small. It does not try to replace every feature of a full GraphQL server. Its main job is:
| Responsibility | What EzyHTTP does |
|---|---|
| HTTP transport | Accepts GraphQL requests over GET /graphql and POST /graphql |
| Query parsing | Parses query names, nested fields, arguments, variables, and named operations |
| Dispatch | Maps each top-level query name to a data fetcher |
| Interception | Runs GraphQL interceptors before and after each fetcher |
| Response shaping | Keeps only the fields selected by the query |
| Error response | Returns GraphQL-style data and errors payloads for known GraphQL errors |
Enabling GraphQL
GraphQL is enabled through application configuration:
graphql.enable=true
When enabled, EzyHTTP creates the GraphQL runtime components, collects registered data fetchers and fetcher providers from the application container, builds a fetcher manager, and registers the
/graphql controller.If the application wants the GraphQL endpoint itself to require authentication through EzyHTTP's normal controller authentication flow, it can also configure:
graphql.authenticated=true
Fetcher-level metadata can still mark individual queries as authenticated, management, or payment-related. That metadata is stored by the fetcher manager and is available to surrounding infrastructure such as interceptors and authorization logic.
Writing a Data Fetcher
A GraphQL data fetcher is the unit that owns one top-level query. The query name can be returned explicitly or declared with the GraphQL query annotation.
@EzySingleton public class ProfileDataFetcher extends GraphQLAbstractDataFetcher { @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"; } }
A client can then ask for only the fields it needs:
query {
profile(userId: 100) {
id
displayName
avatarUrl
}
}
The fetcher receives two important inputs:
| Input | Meaning |
|---|---|
RequestArguments | The current EzyHTTP request context, including values prepared by the HTTP layer or interceptors |
GraphQLQueryDefinition | The parsed top-level query, including arguments and selected fields |
The fetcher returns a Java object. EzyHTTP converts that object into a map and applies field filtering after the fetcher returns.
Request Format
EzyHTTP follows the common GraphQL-over-HTTP shape for both
GET and POST.For
GET, the query is passed as a request parameter:
GET /graphql?query={profile(userId:100){id displayName}}
Variables can be passed as a JSON-encoded
variables parameter:
GET /graphql?query=query($userId:Long){profile(userId:$userId){id}}&variables={"userId":100}
For
POST, the body contains query, optional operationName, and optional variables:
{
"query": "query GetProfile { profile(userId: $userId) { id displayName } }",
"operationName": "GetProfile",
"variables": {
"userId": 100
}
}
Arguments are parsed with a relaxed JSON mapper, so object-style arguments can use unquoted field names and single quotes where appropriate:
{
search(filter: {keyword: 'java', limit: 20}) {
items {
title
}
}
}
How Query Parsing Works
The GraphQL module uses a lightweight parser tailored to EzyHTTP's fetcher model. The parser normalizes the query string, removes redundant separators such as extra whitespace, commas, plus signs, tabs, and newlines, then walks the query character by character.
The result is a tree of query definitions:
profile(userId: 100)
id
displayName
friends
id
name
Each top-level field becomes a separate query definition. This means a single request can contain multiple top-level queries:
{
profile(userId: 100) {
id
displayName
}
notifications {
title
read
}
}
EzyHTTP executes them one by one and places each result under its query name:
{
"data": {
"profile": {
"id": 100,
"displayName": "Alice"
},
"notifications": [
{
"title": "Welcome",
"read": false
}
]
}
}
Named operations are supported through
operationName. When an operation name is supplied, EzyHTTP extracts that operation's selection set and parses only that part.Variables and Arguments
Variables are resolved during argument parsing. When the parser sees a variable reference such as
$userId, it replaces it with the matching value from the variables map before the fetcher receives the query definition.
query GetProfile {
profile(userId: $userId) {
id
displayName
}
}
Inside the fetcher:
Long userId = query.getArgumentValue("userId", Long.class);
Nested field arguments are also available. This is useful when a fetcher wants to inspect options attached to a child field:
{
profile(userId: 100) {
posts(limit: 5) {
title
}
}
}
The query definition lets the fetcher navigate to a nested field and read its argument value.
Fetcher Dispatch
After parsing, the controller loops over top-level query definitions. For each query:
query name -> fetcher manager -> data fetcher
If a fetcher is registered directly for that query name, it is used immediately. If not, EzyHTTP asks registered fetcher providers whether they can provide one. Providers are useful for dynamic query registration or plugin-style modules.
If no fetcher is found, the request fails with a GraphQL error rather than falling back to a REST controller.
Interceptors
GraphQL interceptors run around each top-level query. Before the fetcher is called, EzyHTTP stores the current GraphQL query name, query group, and fetcher manager in the request context, then calls
preHandle.public boolean preHandle( RequestArguments arguments, String queryGroup, String queryName, GraphQLQueryDefinition queryDefinition, GraphQLDataFetcher dataFetcher ) { return true; }
If any interceptor returns
false, EzyHTTP rejects that query and returns an error. After a successful fetch, postHandle receives the current response data and can perform logging, metrics, auditing, or other cross-cutting work.Interceptors are ordered by priority, so applications can put authentication, authorization, logging, and observability in predictable order.
Response Filtering
The fetcher can return a full object, but the client only receives the requested fields.
Suppose the fetcher returns:
{
"id": 100,
"displayName": "Alice",
"email": "alice@example.com",
"roles": ["admin"],
"profile": {
"avatarUrl": "/avatars/alice.png",
"bio": "Java developer"
}
}
And the client asks:
{
profile(userId: 100) {
id
displayName
profile {
avatarUrl
}
}
}
The response contains only:
{
"data": {
"profile": {
"id": 100,
"displayName": "Alice",
"profile": {
"avatarUrl": "/avatars/alice.png"
}
}
}
}
Nested maps and lists of maps are filtered recursively. If the query asks for nested fields but the returned value is not shaped like an object or list of objects, EzyHTTP reports an invalid schema error for that field.
EzyHTTP also supports
* as a convenience field selector to include all non-null fields at that level:
{
profile(userId: 100) {
*
}
}
Error Handling
Known GraphQL errors are returned in a GraphQL-style response:
{
"data": null,
"errors": [
{
"message": "invalid arguments: ..."
}
]
}
Typical error cases include:
| Error case | What happens |
|---|---|
Invalid variables JSON | The request is rejected with an object-mapping error |
| Invalid argument syntax | The parser returns an argument parsing error |
Unknown operationName | The request returns an unknown operation error |
| No fetcher for a query | The request returns a fetcher-not-found error |
| Interceptor rejection | The request returns an interceptor rejection error |
| Response shape mismatch | The request returns an invalid schema error |
What This GraphQL Layer Does Not Do
EzyHTTP's GraphQL support is deliberately pragmatic. It is not a full GraphQL engine with a strongly typed schema language, validation phase, resolver graph, introspection system, subscription support, or automatic database querying.
In practice:
| Feature | Behavior |
|---|---|
| Schema validation | Fetchers may expose schema metadata, but the runtime path is centered on parsing and dispatching queries, not full SDL validation |
| Resolvers | The top-level query is resolved by one fetcher; nested fields are selected from the object returned by that fetcher |
| REST fallback | A GraphQL query is not automatically converted into REST controller calls |
| Authorization | The framework provides request context, authentication flags, query grouping, and interceptors; application logic still decides policy |
| Data loading | Fetchers are responsible for efficient loading, batching, caching, and permission-aware data access |
This design keeps the module small and predictable. The GraphQL endpoint controls query shape, while normal Java services and EzyHTTP infrastructure continue to own business behavior.
A Complete Flow
For a request like:
query GetProfile {
profile(userId: $userId) {
id
displayName
friends {
name
}
}
}
with:
{
"userId": 100
}
EzyHTTP performs the following runtime flow:
flowchart LR
subgraph P[Parse]
A[Read query and variables] --> B[Extract operation]
B --> C[Build query definitions]
C --> D[Resolve arguments]
end
subgraph F[Fetch]
E[Find profile fetcher] --> G[Run preHandle]
G --> H[Call getData]
end
subgraph R[Respond]
I[Convert object to map] --> J[Filter selected fields]
J --> K[Run postHandle]
K --> L[Return data.profile]
end
D --> E
H --> I
Conclusion
GraphQL in EzyHTTP is best understood as a lightweight query-shaping layer on top of the existing EzyHTTP server model. It receives requests through
/graphql, parses top-level queries, dispatches each query to a Java data fetcher, lets interceptors participate in the lifecycle, and filters the fetcher's response according to the requested fields.That makes it a good fit for applications that want GraphQL-style client flexibility while keeping service logic, authentication, interceptors, and object mapping close to the rest of the EzyHTTP stack.