GraphQL on Wildfly swarm
“GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.”
– from https://graphql.org/
Anyone that has built a REST services that is being used by multiple consumers, like other services or web sites or mobile devices, will know that is very hard to build that perfect Endpoint that satisfies all the needs. You typically end up with variations of the same service, for all those special cases :)
Now, we all know we should just be using HATEOAS… and it was on my TODO list (promise !), until I stumbled upon GraphQL.
So in this blog post I explain how you can add GraphQL to you existing JAX-RS application, without too much effort.
Example project
The example project is available in Github, and very easy to get started
git clone https://github.com/phillip-kruger/membership.git cd membership mvn clean install
This will start a fatjar wildfly-swarm with the example application http://localhost:8080/membership/
High level
The example is a basic Membership service, where you can get all members, or a specific member. You can add, edit and remove a member.
The application is a typical JAX-RS, CDI, EJB, JPA, Bean validation Java EE application, and we are adding a new GraphQL Endpoint.
The GraphQL part uses the following libraries:
The only java classes I added to expose my existing JAX-RS as GraphQL:
- MembershipGraphQLListener – to register the “/graphql” servlet listener.
- MembershipGraphQLApi – the GraphQL Endpoint. Just wrapping the existing
@Stateless
service. - MembershipErrorHandler – to handle Exceptions.
Using the annotations from graphQL-spqr, the MembershipGraphQLApi
class really just describes and wraps the existing @Stateless
service:
@RequestScoped public class MembershipGraphQLApi { @Inject private MembershipService membershipService; // ... @GraphQLQuery(name = "memberships") public List<Membership> getAllMemberships(Optional<MembershipFilter> filter, @GraphQLArgument(name = "skip") Optional<Integer> skip, @GraphQLArgument(name = "first") Optional<Integer> first) { return membershipService.getAllMemberships(filter, skip, first); } // ... }
My hope – we will soon have a JAX-QL (or something) as part of Java EE (or Jakarta EE, or MicroProfile) to make this even easier !!
First some REST
I am using MicroProfile OpenAPI and Swagger UI to create Open API definitions for the REST Endpoint.
You can test some queries using http://localhost:8080/membership/rest/openapi-ui/
Example – Getting all memberships:
GET http://localhost:8080/membership/rest
This will return:
[ { "membershipId": 1, "owner": { "id": 1, "names": [ "Natus", "Phillip" ], "surname": "Kruger" }, "type": "FULL" }, { "membershipId": 2, "owner": { "id": 2, "names": [ "Charmaine", "Juliet" ], "surname": "Kruger" }, "type": "FULL" }, { "membershipId": 3, "owner": { "id": 3, "names": [ "Koos" ], "surname": "van der Merwe" }, "type": "FULL" }, { "membershipId": 4, "owner": { "id": 4, "names": [ "Minki" ], "surname": "van der Westhuizen" }, "type": "FREE" } ]
Example – Getting a certain membership (1):
GET http://localhost:8080/membership/rest/1
This will return:
{ "membershipId": 1, "owner": { "id": 1, "names": [ "Natus", "Phillip" ], "surname": "Kruger" }, "type": "FULL" }
Now let’s look at GraphQL
The application includes the GraphiQL UI (as a webjar), that makes it easy to test some GraphQL Queries
You can test some queries using http://localhost:8080/membership/graph/graphiql/
So let’s see if GraphQL delivers on the “No more Over- and Under Fetching” promise.
Get all memberships and all fields (so the same as the REST get all)
query Memberships { memberships{ ...fullMembership } } fragment fullMembership on Membership { membershipId owner{ ...owner } type } fragment owner on Person { id names surname }
This will return all values, however, it’s now easy to define which fields should be included…
Get all memberships but only include the id field
query Memberships { memberships{ ...membershipIdentifiers } } fragment membershipIdentifiers on Membership { membershipId }
The resulting payload is now much smaller:
{ "data": { "memberships": [ { "membershipId": 1 }, { "membershipId": 2 }, { "membershipId": 3 }, { "membershipId": 4 } ] } }
Now lets get only specific types of memberships (so get all FREE memberships)
query FilteredMemberships { memberships(filter:{ type:FREE }){ ...fullMembership } } fragment fullMembership on Membership { membershipId owner{ ...owner } type } fragment owner on Person { id names surname }
This will return just the free memberships. Cool !
Or even better, all members that’s surname starts with “Kru”
query FilteredMemberships { memberships(filter:{ surnameContains: "Kru" }){ ...fullMembership } } fragment fullMembership on Membership { membershipId owner{ ...owner } type } fragment owner on Person { id names surname }
Great !! We found two people:
{ "data": { "memberships": [ { "membershipId": 1, "owner": { "id": 1, "names": [ "Natus", "Phillip" ], "surname": "Kruger" }, "type": "FULL" }, { "membershipId": 2, "owner": { "id": 2, "names": [ "Charmaine", "Juliet" ], "surname": "Kruger" }, "type": "FULL" } ] } }
Getting a certain membership, using a variable on the client:
query Membership($id:Int!) { membership(membershipId:$id){ ...fullMembership } } fragment fullMembership on Membership { membershipId owner{ ...owner } type } fragment owner on Person { id names surname }
The variable:
{"id":1}
Include fields on a certain condition:
query Membership($id:Int!,$withOwner: Boolean!) { membership(membershipId:$id){ ...fullMembership } } fragment fullMembership on Membership { membershipId owner @include(if: $withOwner){ ...owner } type } fragment owner on Person { id names surname }
The variable:
{"id":1,"withOwner": false}
this will exclude the owner (true to include):
{ "data": { "membership": { "membershipId": 1, "type": "FULL" } } }
Pagination
Let’s use the get all query, but paginate.
query Memberships($itemsPerPage:Int!,$pageNumber:Int!) { memberships( first:$itemsPerPage, skip:$pageNumber) { membershipId owner{ names surname } type } }
The variable:
{"itemsPerPage": 2,"pageNumber": 1}
This will return the first 2 results, and then you can page by increasing the “pageNumber” value.
Mutations
Create
mutation CreateMember { createMembership(membership: {type:FULL,owner: {names: "James",surname:"Small"}}) { membershipId } }
This will create the new membership and return the id.
Update
mutation EditMember($membership: MembershipInput!) { createMembership(membership:$membership) { membershipId } }
The variable:
{ "membership": { "membershipId": 2, "owner": { "names": [ "Charmaine", "Juliet" ], "surname": "Krüger" }, "type": "FULL" } }
(added a umlaut on the u of Kruger, now it should be Krüger)
Delete
mutation DeleteMembership($id:Int!){ deleteMembership(membershipId:$id){ membershipId } }
The variable:
{"id":1}
This will delete membership 1.
Exception.
The MembershipErrorHandler translates a ConstraintViolationException (that is thrown when the bean validation fails) and creates a nice error message for GraphQL.
So let’s try and create a member with a surname of just one letter.
mutation CreateMember($membership: MembershipInput!) { createMembership(membership:$membership) { membershipId } }
The variable:
{ "membership": { "owner": { "names": "Christina", "surname": "S" }, "type": "FULL" } }
This will return the bean validation error message:
{ "data": { "createMembership": null }, "errors": [ { "message": "Surname 'S' is too short, minimum 2 characters", "path": null, "extensions": null } ] }
If you look at the Person POJO:
@NotNull(message = "Surname can not be empty") @Size(min=2, message = "Surname '${validatedValue}' is too short, minimum {min} characters") private String surname;
Introspection
The other nice thing about GraphQL is that it has a Schema & Type System that you can query:
{ __schema { queryType { name fields { name } } mutationType{ name fields{ name } } subscriptionType { name fields{ name } } } }
Above will describe the queries and mutations available on this endpoint.
You can also describe your Models:
{ __type(name: "Membership") { name kind fields { name args { name } } } }
Summary
In this example we did not remove REST, but just added GraphQL as an alternative option for the consumer.
By now it should be clear that the client has a lot more options to filter and query the data exactly like they need it. All this without the server having to do any extra work. This allows for rapid product iterations on the client side.
The payload over the wire is optimized and we are saving bandwidth !
Again, my hope – we will soon have a JAX-QL (or something) as part of Java EE (or Jakarta EE, or MicroProfile) to make this even easier !!
Published on Java Code Geeks with permission by Phillip Krüger, partner at our JCG program. See the original article here: GraphQL on Wildfly swarm Opinions expressed by Java Code Geeks contributors are their own. |