GraphQL Server

The XOOM GraphQL server for smooth legacy integration with complex queries made simple.

The XOOM GraphQL server component is based on XOOM HTTP. The GraphQLResource is a dynamic resource handler implemented as the Reactive HTTP endpoint used to submit queries to our GraphQL server for asynchronous execution. The GraphQLResource registers the URL /graphql as the only HTTP resource specific to GraphQL applications, and thus handles all GraphQL operations. The single endpoint is logical and actually scales with a configurable number of backing actors. As expected, our GraphQL server is end-to-end Reactive.

Queries and Mutations

Queries can be as simple as asking for specific fields and/or nested fields on objects. Queries can accept arguments to fields.

More advanced concepts such as aliases, fragments, operation name, variables, and directives are supported as well. While queries perform data fetching, mutations perform state transformations. The following is a basic example of a find query handled by our GraphQL server:

query {
    bookById(id: "book-1") {
        id
        name
        pageCount
        author {
            firstName
            lastName
        }
    }
}

And the following is an example of mutation on a resource entity. The upsert query means to insert or update as needed, making it an idempotent operation:

mutation {
    upsertBook(id: "book-8", name: "The Mysterious Stranger", pageCount: 200, authorId: "author-2") {
        id
        name
        pageCount
        author {
            firstName
            lastName
        }
    }
}

Schemas and Types

XOOM GraphQL adopts a schema-first approach. The design of a GraphQL API starts with defining a schema. The grammar used for writing schemas is called Schema Definition Language (SDL). A schema definition supports all GraphQL concepts, such as object types, fields, arguments, scalar types, enumeration types, lists and non-null, as well as interfaces and union types. Server side integration with a specific schema is done by following naming conventions. Classes and methods must match with schema definition components. Queries are validated, and any validation mismatches are signaled as failures. Here is an example schema:

# The Root Query for the application
type Query {
  bookById(id: ID): Book 
}

# The Root Mutation for the application
type Mutation {
  upsertBook(id: ID!, name: String!, pageCount: Int, authorId: ID!): Book
}

type Book {
  id: ID
  name: String
  pageCount: Int
  author: Author
}

type Author {
  id: ID
  firstName: String
  lastName: String
}

The schema types Book and Author map to class Book and Author, respectively:

public class Book {
    private String id;
    private String name;
    private int pageCount;
    private Author author;

    public Book(String id, String name, int pageCount, Author author) {
        this.id = id;
        this.name = name;
        this.pageCount = pageCount;
        this.author = author;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getPageCount() {
        return pageCount;
    }

    public Author getAuthor() {
        return author;
    }

    public Book updateWith(final Book from) {
        this.name = from.name;
        this.pageCount = from.pageCount;
        this.author = from.author;

        return this;
    }
}
public class Author {
    private String id;
    private String firstName;
    private String lastName;

    public Author(String id, String firstName, String lastName) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

The next topic is resolvers, which link data query results to schemas.

Resolvers

Each query or mutation must have a resolver that is associated by a naming convention. A resolver is responsible for actually performing a specific operation. All resolvers are registered to a GraphQLProcessor. The GraphQLProcessor is used by the previously discussed GraphQLResource. The GraphProcessor is fully Reactive, being implemented as an actor.

This is a basic (and incomplete) query resolver type:

package io.vlingo.xoom.graphql.resolvers;

import graphql.kickstart.tools.GraphQLQueryResolver;
import io.vlingo.xoom.graphql.Store;
import io.vlingo.xoom.graphql.model.Book;

public class BookQuery implements GraphQLQueryResolver {
    public Book getBookById(String bookId) {
        // query handling...
    }
}

And here is a mutation resolver example (also incomplete):

package io.vlingo.xoom.graphql.resolvers;

import graphql.kickstart.tools.GraphQLMutationResolver;
import io.vlingo.xoom.graphql.Store;
import io.vlingo.xoom.graphql.model.Book;

public class BookMutation implements GraphQLMutationResolver {
    public Book upsertBook(final String id, final String name, final int pageCount, final String authorId) {
        // mutation handling...
    }

    private Book upsertBook(final Book book) {
        // mutation handling...
    }
}

For more complete examples, see the example source code.

Client Integration

XOOM GraphQL supports seamless client side integration that makes for smooth query and mutation operations. There are generated classes for types, queries, mutations, requests, responses, and projections. Both the server side and client side are fully Reactive. Our Reactive HTTP client provided by XOOM HTTP can use the generated classes end-to-end.

The following is an example of a Reactive client consuming a GraphQL response from a query that is executed reactively; note the use of .andFinallyConsume(...):

package io.vlingo.xoom.graphql.integration;

import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperationRequest;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLRequest;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLResponseProjection;

import io.vlingo.xoom.actors.World;
import io.vlingo.xoom.http.resource.Client;
import io.vlingo.xoom.http.resource.Server;

public class BookStoreTest {
    private static final String host = "localhost";
    private static final int port = 8080;

    private static World world;
    private static Server server;
    private static Client client;
    
    ...

    private Response performRequest(final GraphQLOperationRequest request, final GraphQLResponseProjection responseProjection) {
        final GraphQLRequest graphQLRequest = new GraphQLRequest(request, responseProjection);
        final String graphQLQuery = graphQLRequest.toHttpJsonBody();

        final AccessResponseConsumer responseConsumer = new AccessResponseConsumer();

        client.requestWith(
                Request
                        .has(Method.POST)
                        .and(URI.create("/graphql"))
                        .and(RequestHeader.contentLength(graphQLQuery))
                        .and(RequestHeader.keepAlive())
                        .and(Body.from(graphQLQuery)))
                .andFinallyConsume(responseConsumer::consume);

        return responseConsumer.accessResponse();
    }
    
    @BeforeAll
    public static void setUp() throws Exception {
        world = World.startWithDefaults("xoom-graphql-test");

        GraphQLProcessor processor = TestBootstrap.newProcessor(world.stage());
        GraphQLResource graphQLResource = new GraphQLResource(world.stage(), processor);

        server = Server.startWith(world.stage(), Resources.are(graphQLResource.routes()), port, Configuration.Sizing.define(),
                new Configuration.Timing(4L, 2L, 100L));

        Address address = Address.from(Host.of(host), port, AddressType.NONE);
        client = Client.using(Client.Configuration.defaultedKeepAliveExceptFor(world.stage(), address, new UnknownResponseConsumer()));
    }
}

Don't forget testing your client-side and server-side query and mutation components!

Unit Tests

End-to-end unit tests cover client side tests as well as server side tests together with full communication between both parties.

The classes in the io.vlingo.xoom.graphql.client.model package are generated based on schema definition. The following is an example unit test:

package io.vlingo.xoom.graphql.integration;

import io.vlingo.xoom.graphql.client.model.*;

public class BookStoreTest {
@Test
    public void queryExistingBookTest() {
        final BookByIdQueryRequest bookRequest = BookByIdQueryRequest.builder()
                .setId("book-1")
                .build();

        final AuthorResponseProjection authorResponseProjection = new AuthorResponseProjection()
                .firstName()
                .lastName();

        final BookResponseProjection bookResponseProjection = new BookResponseProjection()
                .id()
                .name()
                .pageCount()
                .author(authorResponseProjection);

        Response response = performRequest(bookRequest, bookResponseProjection);
        Assertions.assertEquals("200", response.statusCode);

        BookByIdQueryResponse queryResponse = JsonSerialization.deserialized(response.entity.content(), BookByIdQueryResponse.class);
        Assertions.assertEquals(0, queryResponse.getErrors().size());

        Book book = queryResponse.bookById();
        Assertions.assertEquals("book-1", book.getId());
        Assertions.assertEquals("Romeo and Juliet", book.getName());
        Assertions.assertEquals("William", book.getAuthor().getFirstName());
        Assertions.assertEquals("Shakespeare", book.getAuthor().getLastName());
    }
}

See the source code for more examples.

Last updated