EzyHTTP how GraphQL works

Updated at 1783872859000
EzyHTTP 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:
ResponsibilityWhat EzyHTTP does
HTTP transportAccepts GraphQL requests over GET /graphql and POST /graphql
Query parsingParses query names, nested fields, arguments, variables, and named operations
DispatchMaps each top-level query name to a data fetcher
InterceptionRuns GraphQL interceptors before and after each fetcher
Response shapingKeeps only the fields selected by the query
Error responseReturns 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:
InputMeaning
RequestArgumentsThe current EzyHTTP request context, including values prepared by the HTTP layer or interceptors
GraphQLQueryDefinitionThe 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 caseWhat happens
Invalid variables JSONThe request is rejected with an object-mapping error
Invalid argument syntaxThe parser returns an argument parsing error
Unknown operationNameThe request returns an unknown operation error
No fetcher for a queryThe request returns a fetcher-not-found error
Interceptor rejectionThe request returns an interceptor rejection error
Response shape mismatchThe 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:
FeatureBehavior
Schema validationFetchers may expose schema metadata, but the runtime path is centered on parsing and dispatching queries, not full SDL validation
ResolversThe top-level query is resolved by one fetcher; nested fields are selected from the object returned by that fetcher
REST fallbackA GraphQL query is not automatically converted into REST controller calls
AuthorizationThe framework provides request context, authentication flags, query grouping, and interceptors; application logic still decides policy
Data loadingFetchers 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.

Table Of Contents