GraphQL - when REST API is
not enough - lessons learned
Part 1
Marcin Stachniuk
8 October 2018
Marcin Stachniuk
mstachniuk.github.io
/mstachniuk/graphql-java-example
@MarcinStachniuk
wroclaw.jug.pl
shipkit.org
Agenda
11:00
● Why?
● Concepts
● Basics
12:00 Lunch
13:00
● GraphQL in Java
● Lessons learned
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Monady w .NET – i tak używasz, więc dowiedz się, o co chodzi.
Mateusz Stasch
Rewolucja w analizie danych z RevoscaleR oraz RevoscalePy
Tomasz Krawczyk
Scale it the easy way - introduction to micro frontends
Jakub Holak
Who is using REST?
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
REST - REpresentational State Transfer
https://api.example.com/customers/123
DELETE
PUT
POST
GET
PATCH
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
REST fixed response
GET /customers/111
{
"customer": {
"id": "111",
"name": "John Doe",
"email": "john@doe.com",
"company": {
"id": "222"
},
"orders": [
{
"id": "333"
},
{
"id": "444"
}
]
}
}
{
"customer": {
"id": "111",
"name": "John Doe",
"email": "john@doe.com",
"company": {
"href": "https://api.example.com/companies/222"
},
"orders": [
{
"href": "https://api.example.com/orders/333"
},
{
"href": "https://api.example.com/orders/444"
}
]
}
}
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
REST consequences: several roundtrips
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
U
nderFetching
REST response with nested data
GET /customers/111
{
"customer": {
"id": "111",
"name": "John Doe",
"email": "john@doe.com",
"company": {
"id": "222",
"name": "My Awesome Corporation",
"website": "MyAwesomeCorporation.com"
},
"orders": [
{
"id": "333",
"status": "delivered",
"items": [
{
"id": "555",
"name": "Silver Bullet",
"amount": "42",
"price": "10000000",
"currency": "USD",
"producer": {
"id": "777",
"name": "Lorem Ipsum",
"website": "LoremIpsum.com"
}
}
]
},
{
"id": "444",
"name": "Golden Hammer",
"amount": "5",
"price": "10000",
"currency": "USD",
"producer": {
...
}
}
] } }
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
O
verFetching
REST response with nested data and limit fields
GET /customers/111?fields=name,company/*,orders.status,orders.items(name,producer/name)
{
"customer": {
"id": "111",
"name": "John Doe",
"email": "john@doe.com",
"company": {
"id": "222",
"name": "My Awesome Corporation",
"website": "MyAwesomeCorporation.com"
},
"orders": [
{
"id": "333",
"status": "delivered",
"items": [
{
"id": "555",
"name": "Silver Bullet",
"amount": "42",
"price": "10000000",
"currency": "USD",
"producer": {
"id": "777",
"name": "Lorem Ipsum",
"website": "LoremIpsum.com"
}
}
]
},
{
"id": "444",
"name": "Golden Hammer",
"amount": "5",
"price": "10000",
"currency": "USD",
"producer": {
...
}
}
] } }
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
REST response with projection
GET /customers/111?projection=customerPreview
{
"customer": {
"id": "111",
"name": "John Doe",
"email": "john@doe.com",
"company": {
"id": "222",
"name": "My Awesome Corporation",
"website": "MyAwesomeCorporation.com"
},
"orders": [
{
"id": "333",
"status": "delivered",
"items": [
{
"id": "555",
"name": "Silver Bullet",
"amount": "42",
"price": "10000000",
"currency": "USD",
"producer": {
"id": "777",
"name": "Lorem Ipsum",
"website": "LoremIpsum.com"
}
}
]
},
{
"id": "444",
"name": "Golden Hammer",
"amount": "5",
"price": "10000",
"currency": "USD",
"producer": {
...
}
}
] } }
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Different clients - different needs
/web /iphone /android /tv
Application
Web iPhone Android TV
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Different clients - different needs
/web /iphone /android /tv
Application
Web iPhone Android TV
Content-Type: application/vnd.myawesomecorporation.com+v1+web+json
iphone
android
tv
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Different clients - different needs
/web /iphone /android /tv
Application
Web iPhone Android TV
Content-Type: application/vnd.myawesomecorporation.com+v1+web+json
iphone
android
tv
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
REST contract is rigid
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Platform Architecture
Platform
App 1 App 2
Customer
App X...
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
REST API Versioning
GET /v2/customers/111RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GET /customers/111?v=v2RE
ST
GET /customers/111 Accept header: application/vnd.myapp.2+jsonRE
ST
GET /customers/111 Custom header: x-ms-version:2RE
ST
● How to migrate?
● How to document?
● Is versionless API possible?
● What about continuous evolution of API?
Modularisation at UI
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
New Frontend Framework
ReactJS
Relay GraphQL
TypeScript
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL
● Graph Query Language
● Published by Facebook in 2015
● Growth from Facebook Graph API
● Reference implementation in JavaScript
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL main concepts
● One endpoint for all operations
● Always define in request what you need
● Queries, Mutations and Subscriptions
● Defined by schema
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Data is a graph
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL time for demo
● Fragments
● Aliases
● Directives
● Interfaces
● Unions
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
● Query
● Syntax Error
● Mutation
● Operation name
● Variables
GraphQL Simple API
GET /customers/2?fields=id,name,email
type Customer {
#fields with ! are not null
id: ID!
name: String!
email: String!
}
type Query {
customer(id: String!): Customer!
}
{
"data": {
"customer": {
"id": "2",
"name": "name",
"email": "a@b.com"
}
}
}
{
customer(id: "2") {
id
name
email
}
}
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL Bad Request
GET /custo!@#$ -> 404
{
"data": null,
"errors": [
{
"message": "Invalid Syntax",
"locations": [
{
"line": 2,
"column": 8
}
],
"errorType": "InvalidSyntax",
"path": null,
"extensions": null
} ] }
{
custo!@#$
}
RE
ST
http.cat/200
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Go back to the roots
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL Simple API
GET /customers/2?fields=id,name,email,company(id,name)
type Customer {
id: ID!
name: String!
email: String!
company: Company
}
type Company {
id: ID!
name: String!
website: String!
}
type Query {
customer(id: String!): Customer!
}
{
"data": {
"customer": {
"id": "2",
"name": "name",
"email": "a@b.com",
"company": {
"id": "211",
"name": "Company Corp."
}
}
}
}
{
customer(id: "2") {
id
name
email
company {
id
name
}
}
}
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL Simple API
GET /customers/2?fields=id,name,email,orders(id,status)
type Customer {
id: ID!
name: String!
email: String!
company: Company
orders: [Order]
}
type Order {
id: ID!
status: Status
}
enum Status {
NEW, CANCELED, DONE
}
{
"data": {
"customer": {
"id": "2",
"name": "name",
"orders": [
{
"id": "55",
"status": "NEW"
},
{
"id": "66",
"status": "DONE"
}
] } } }
{
customer(id: "2") {
id
name
orders {
id
status
}
}
}
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL mutations
input CreateCustomerInput {
name: String
email: String
clientMutationId: String!
}
type CreateCustomerPayload {
customer: Customer
clientMutationId: String!
}
type Mutation {
createCustomer(input: CreateCustomerInput):
CreateCustomerPayload!
}
{
"data": {
"createCustomer": {
"customer": {
"id": "40",
},
"clientMutationId":
"123"
}
}
}
POST /customers PUT /customers/123 DELETE /customers/123 PATCH /customers/123
mutation {
createCustomer(input: {
name: "MyName"
email: "me@me.com"
clientMutationId: "123"
}) {
customer {
id
}
clientMutationId
}
}
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL mutations + operation name
mutation crCust {
createCustomer(input: {
name: "MyName"
email: "me@me.com"
clientMutationId: "123"
}) {
customer {
id
}
clientMutationId
}
}
{
"data": {
"createCustomer": {
"customer": {
"id": "40",
},
"clientMutationId":
"123"
}
}
}
POST /customers PUT /customers/123 DELETE /customers/123 PATCH /customers/123RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
mutation {
createCustomer(input: {
name: "MyName"
email: "me@me.com"
clientMutationId: "123"
}) {
customer {
id
}
clientMutationId
}
}
GraphQL Variables
{
"data": {
"createCustomer": {
"customer": {
"id": "40",
},
"clientMutationId":
"123"
}
}
}
POST /customers PUT /customers/123 DELETE /customers/123 PATCH /customers/123
mutation crCust {
createCustomer(input: {
name: "MyName"
email: "me@me.com"
clientMutationId: "123"
}) {
customer {
id
}
clientMutationId
}
}
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
mutation crCust ($input: CreateCustInput) {
createCustomer(input: $input) {
customer {
id
}
clientMutationId
}
}
{
"input": {
"name": "MyName 2",
"email": "me2@me.com",
"clientMutationId": "123"
}
}
GraphQL Aliases
GET /customers/2?fields=id,name,email +
{
"data": {
"cust1": {
"id": "2",
"name": "name",
"email": "a@b.com"
},
"cust2": {
"id": "3",
"name": "John Doe",
"email": "john@doe.com"
}
}
}
query get2Cust {
cust1: customer(id: "2") {
id
name
email
}
cust2: customer(id: "3") {
id
name
email
}
}
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GET /customers/3?fields=id,name,emailRE
ST
GraphQL Fragment
GET /customers/2?fields=id,name,email +
{
"data": {
"cust1": {
"id": "2",
"name": "name",
"email": "a@b.com"
},
"cust2": {
"id": "3",
"name": "John Doe",
"email": "john@doe.com"
}
}
}
query get2Cust {
cust1: customer(id: "2") {
... frag1
}
cust2: customer(id: "3") {
... frag1
}
}
fragment frag1 on Customer {
id
name
email
}
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GET /customers/3?fields=id,name,emailRE
ST
GraphQL Directive
GET /customers/2?fields=id,name
{
"data": {
"customer": {
"id": "2",
"name": "name"
}
}
}
query getCust ($showEmail: Boolean!) {
customer(id: "2") {
id
name
email @include(if: $showEmail)
}
}
{
"showEmail": false
}
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL Interface
GET /users?fields=id,name,superAdmin,permissions
type Query {
users: [User]
}
interface User {
id: ID!
name: String!
email: String!
}
type Admin implements User {
superAdmin: Boolean! // + id...
}
type Moderator implements User {
permissions: [String] // + id...
}
{
"data": {
"users": [
{
"id": "777",
"name": "Admin a",
"superAdmin": true
},
{
"id": "888",
"name": "Moderator",
"permissions": [
"Delete Customer",
"Delete comment"
]}]}}
query getUsers {
users {
... on Admin {
id
name
superAdmin
}
... on Moderator {
id
name
permissions
}
}
}
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL Union
GET /search?input=a?fields=typename,name
type Query {
search(input: String):
[SearchResult]
}
union SearchResult =
Customer |
Admin |
Moderator
{
"data": {
"search": [
{
"__typename": "Customer",
"name": "name"
},
{
"__typename": "Admin",
"name": "Admin a"
},
{
"__typename": "Moderator",
"name": "Moderator"
}]}}
query searchSmth {
search(input: "a") {
__typename
... on Customer {
name
}
... on Admin {
name
}
... on Moderator {
name
}
}
}
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Q&A
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Feedback
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
To be continued...
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL - when REST API is
not enough - lessons learned
Part 2
Marcin Stachniuk
8 October 2018
Agenda
11:00
● Why?
● Concepts
● Basics
12:00 Lunch
13:00
● GraphQL in Java
● Lessons learned
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Serverless .NET on AWS
Wojciech Gawroński
Lunch
GraphQL
● Graph Query Language
● Published by Facebook in 2015
● Growth from Facebook Graph API
● Reference implementation in JavaScript
● First version of Java Library: 18 Jul 2015
https://github.com/graphql-java/graphql-java
● First usage: 21 Sep 2015
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Lessons Learned #1
Never add a library to your project
few days after init release
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
● No community
● A lot of bugs
● Bad documentation
● Strict following reference
implementation and specification
DO NOT TRY
THIS AT WORK
GraphQL Simple API
GET /customers/2?fields=id,name,email,orders(id,status)
type Customer {
id: ID!
name: String!
email: String!
company: Company
orders: [Order]
}
type Order {
id: ID!
status: Status
}
enum Status {
NEW, CANCELED, DONE
}
{
"data": {
"customer": {
"id": "2",
"name": "name",
"orders": [
{
"id": "55",
"status": "NEW"
},
{
"id": "66",
"status": "DONE"
}
] } } }
{
customer(id: "2") {
id
name
orders {
id
status
}
}
}
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
How to implement DataFetcher for queries
GET /customers/2?fields=id,name,email,orders(id,status)
@Component
public class CustomerFetcher extends PropertyDataFetcher<Customer> {
@Autowired
private CustomerService customerService;
@Override
public Customer get(DataFetchingEnvironment environment) {
String id = environment.getArgument("id");
return customerService.getCustomerById(id);
}
}
RE
ST
{
customer(id: "2") {
id
name
orders {
id
status
}
}
}
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
How to implement DataFetcher for queries
GET /customers/2?fields=id,name,email,orders(id,status)
public class Customer {
private String id;
private String name;
private String email; // getters are not required
}
RE
ST
{
customer(id: "2") {
id
name
orders {
id
status
}
}
}
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
public class OrderDataFetcher extends PropertyDataFetcher<List<Order>> {
@Override
public List<Order> get(DataFetchingEnvironment environment) {
Customer source = environment.getSource();
String customerId = source.getId();
return orderService.getOrdersByCustomerId(customerId);
}
}
GraphQL mutations
input CreateCustomerInput {
name: String
email: String
clientMutationId: String!
}
type CreateCustomerPayload {
customer: Customer
clientMutationId: String!
}
type Mutation {
createCustomer(input: CreateCustomerInput):
CreateCustomerPayload!
}
{
"data": {
"createCustomer": {
"customer": {
"id": "40",
},
"clientMutationId":
"123"
}
}
}
POST /customers PUT /customers/123 DELETE /customers/123 PATCH /customers/123
mutation {
createCustomer(input: {
name: "MyName"
email: "me@me.com"
clientMutationId: "123"
}) {
customer {
id
}
clientMutationId
}
}
RE
ST
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
How to implement DataFetcher for mutations
POST /customers PUT /customers/123 DELETE /customers/123 PATCH /customers/123
@Component
public class CreateCustomersFetcher extends
PropertyDataFetcher<CreateCustomersPayload> {
@Override
public CreateCustomerPayload get(DataFetchingEnvironment env) {
Map<String, Object> input = env.getArgument("input");
String name = (String) input.get("name");
String email = (String) input.get("email");
String clientMutationId = (String) input.get("clientMutationId");
Customer customer = customerService.create(name, email);
return new CreateCustomerPayload(customer, clientMutationId);
}
RE
ST
mutation {
createCustomer(input: {
name: "MyName"
email: "me@me.com"
clientMutationId: "123"
}) {
customer {
id
}
clientMutationId
}
}
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
In case Interfaces and Unions - Type Resolver is needed
public class UserTypeResolver implements TypeResolver {
@Override
public GraphQLObjectType getType(TypeResolutionEnvironment env) {
Object javaObject = env.getObject();
if (javaObject instanceof Admin) {
return env.getSchema().getObjectType("Admin");
} else if (javaObject instanceof Moderator) {
return env.getSchema().getObjectType("Moderator");
} else {
throw new RuntimeException("Unknown type " + javaObject.getClass().getName());
}
}
}
public interface User {...}
public class Admin implements User {...}
public class Moderator implements User {...}
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Glue everything together
RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
.type("Query", builder ->
builder.dataFetcher("customer", customerFetcher))
.type("Mutation", builder ->
builder.dataFetcher("createCustomer", createCustomerFetcher))
.type(newTypeWiring("User")
.typeResolver(new UserTypeResolver())
.build())
...
.build();
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Abstraction over GraphQL Java
Our abstraction
Data Fetcher 2
Inputs mapping to objects
Schema definition
Pagination
...
Data Fetcher 1 Data Fetcher N...
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Lessons Learned #2
Abstraction is not good if you don’t understand
how it works under the hood
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
● Copy paste errors
● Wrong usage
● Hard to update to new version
GraphQL type system
How to define your schema?
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Code First approach
private GraphQLFieldDefinition customerDefinition() {
return GraphQLFieldDefinition.newFieldDefinition()
.name("customer")
.argument(GraphQLArgument.newArgument()
.name("id")
.type(new GraphQLNonNull(GraphQLString)))
.type(new GraphQLNonNull(GraphQLObjectType.newObject()
.name("Customer")
.field(GraphQLFieldDefinition.newFieldDefinition()
.name("id")
.description("fields with ! are not null")
.type(new GraphQLNonNull(GraphQLID))
.build())
….
.build()))
.dataFetcher(customerFetcher)
.build();
}
Schema First approach
type Query {
customer(id: String!): Customer!
}
type Customer {
#fields with ! are not null
id: ID!
name: String!
email: String!
company: Company
orders: [Order]
}
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Code First approach - How to build
Introspection
query
Introspection
response
Replace Relay
definitions
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Typescript relay
plugin
Schema First approach
type Customer {
# fields with ! are required
id: ID!
name: String!
email: String!
company: Company
orders: [Order]
}
*.graphqls
SchemaParser schemaParser = new SchemaParser();
File file = // ...
TypeDefinitionRegistry registry = schemaParser.parse(file);
SchemaGenerator schemaGenerator = new SchemaGenerator();
RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
.type("Query", builder ->
builder.dataFetcher("customer", customerFetcher))
// ...
.build();
return schemaGenerator.makeExecutableSchema(registry, runtimeWiring);
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Schema First approach - project building diagram
model.graphqls
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL SPQR
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
https://github.com/leangen/graphql-spqr
SPQR - Senatus Populusque Romanus
SPQR approach
public class UserService {
@GraphQLQuery(name = "user")
public User getById(@GraphQLArgument(name = "id") Integer id) {
//...
}
}
public class Customer {
//...
@GraphQLQuery(name = "name", description = "A person's name")
public String getName() {
return name;
}
}
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Lessons Learned #3
Code First Approach is the worst
Schema First Approach:
● Easy to maintain and
understand
● Helps organise work
● Demo schema is 3x smaller
GraphQL SPQR - ?
Code First approach:
● Hard to maintain
● It was the only way at the
beginning to define a schema
● No possibility to mix both
● No easy way to migrate to
Schema First
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
From Code First to Schema First migration
https://github.com/mstachniuk/graphql-schema-from-introspection-generator
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL - How to define pagination, filtering, sorting?
Pagination:
● before, after
● offset, limit
Filtering:
● filter(name: “Bob” email: “%@gmail.com”)
● filter: {
OR: [{
email: “%@gmail.com”
}]
}, name: “Bob”
}
Sorting:
● orderBy: ASC, DESC
● sort: NEWEST, IMPORTANCE
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Lessons Learned #4
GraphQL is not full query language
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
● Flexibility
● Less common conventions
● Dgraph.io created GraphQL+-
GraphQL downsides
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL downsides: N+1 problem
{
customers { 1 call
id
name
orders { n calls
id
status
}
}
}
java-dataloader
● Add async BatchLoader
● Add caching
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
graphQL = GraphQL.newGraphQL(schema)
.queryExecutionStrategy(
new BatchedExecutionStrategy())
.build();
+ @Batched in DataFetcher#get()
Lessons Learned #5
If you have N + 1 problem
use java-dataloader
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Complex Queries
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
...
}
}
}
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
graphQL = GraphQL.newGraphQL(schema)
.instrumentation(
new ChainedInstrumentation(asList(
new MaxQueryComplexityInstrumentation(200),
new MaxQueryDepthInstrumentation(20)
)))
.build();
Lessons Learned #6
Performance killer?
Define instrumentation!
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL downsides: Bad GraphQL API definition - examples
{
customer(id: "2") { … }
customerFull(id: "2") { … }
customerFull2(id: "2") { … }
customerWithDetails(id: "2") { … }
...
}
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
{
customer(id: "2") {
id
name
orders {
id
status
}
}
}
GraphQL downsides: Bad GraphQL API definition - examples
query getUser {
user(id: "123") {
... on Admin {
id
superAdmin
}
... on Moderator {
id
permissions
}
}
}
{
admin(id: "123") {
id
superAdmin
}
moderator(id: "123") {
id
permissions
}
}
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL downsides: Bad GraphQL API definition - examples
{
orders (input: {
status: "NEW"
first: "2"
offset: "3"
}, first: "1", offset: "3") {
Items { … }
}
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Lessons Learned #7
Thinking shift is a key
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
● Let’s think in graphs and NOT in
endpoints / resources / entities / DTOs
● Bad design of our API
GraphQL Testing
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Testing GraphQL
@SpringBootTest
@ContextConfiguration(classes = Main)
class CustomerFetcherSpec extends Specification {
@Autowired
GraphQLSchema graphQLSchema
GraphQL graphQL
def setup() {
graphQL = GraphQL.newGraphQL(graphQLSchema).build()
}
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Testing GraphQL
def "should get customer by id"() {
given:
def query = """{ customer(id: "2") { … } }"""
def expected = [ "customer": [ … ] ]
when:
def result = graphQL.execute(query)
then:
result.data == expected
}
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Lessons Learned #8
Testing is easy
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Trap Adventure 2 - "The Hardest Retro Game"
Versioning
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL takes a strong opinion on avoiding
versioning by providing the tools for the
continuous evolution of a GraphQL schema.
Src: https://graphql.org/learn/best-practices/#versioning
Versioning
type Query {
customer(id: String!): Customer! @deprecated(reason: "not used ...")
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Lessons Learned #9
Versioning is not a problem
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GET /v2/customers/111RE
ST
GET /customers/111?v=v2RE
ST
GET /customers/111 Accept header: application/vnd.myapp.2+jsonRE
ST
GET /customers/111 Custom header: x-ms-version:2RE
ST
Versioning problem is different
Tools
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Tools GraphiQL
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Tools Relay
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
user(...) {
photo(width: "120", height: "120")
}
user(...) {
name
}
user(...) {
email
}
user(...) {
name
email
photo(width: "120", height: "120")
}
Distelli/graphql-apigen
https://github.com/Distelli/graphql-apigen
Generate Java APIs with GraphQL Schemas in order to
facilitate "schema first" development.
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL Java Tools
https://github.com/graphql-java/graphql-java-tools
Map GraphQL schema to POJOs
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
More libraries and projects related to graphql-java
https://github.com/graphql-java/awesome-graphql-java
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
● Generates a GraphQL schema from a JDBC data source
● Annotations-based syntax for GraphQL schema definition
● JPA Implementation of GraphQL (builds on graphql-java)
● And more examples
Apollo
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Build a universal GraphQL API on top of your existing REST
APIs, so you can ship new application features fast without
waiting on backend changes.
Lessons Learned #10
Tooling is nice
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
now
Summary
GraphQL Pros:
● Nice alternative to REST
● It can be used together with REST
● Good integration with Relay / ReactJS
● You get exactly what you want to get
● Good for API with different clients
● Good to use on top of existing API
● Self documented
● Easy testing
● Nice tooling
● Thin layer
GraphQL Cons:
● High entry barrier
● Hard to return simple Map
● Not well know (yet)
● Performance overhead
● A lot of similar code to write
● No Operation Idempotency
● No Cache & Security
● No Authorisation
● Always HTTP 200 -
unsupported tools
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Nothing is a silver bullet
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Q&A
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
Feedback
@MarcinStachniukGraphQL - when REST API is not enough - lessons learned
GraphQL - when REST API is
not enough - lessons learned
Part 2
Marcin Stachniuk
8 October 2018
Thank
you!

GraphQL - when REST API is to less - lessons learned

  • 1.
    GraphQL - whenREST API is not enough - lessons learned Part 1 Marcin Stachniuk 8 October 2018
  • 2.
  • 3.
    Agenda 11:00 ● Why? ● Concepts ●Basics 12:00 Lunch 13:00 ● GraphQL in Java ● Lessons learned @MarcinStachniukGraphQL - when REST API is not enough - lessons learned Monady w .NET – i tak używasz, więc dowiedz się, o co chodzi. Mateusz Stasch Rewolucja w analizie danych z RevoscaleR oraz RevoscalePy Tomasz Krawczyk Scale it the easy way - introduction to micro frontends Jakub Holak
  • 4.
    Who is usingREST? @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 5.
    REST - REpresentationalState Transfer https://api.example.com/customers/123 DELETE PUT POST GET PATCH @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 6.
    REST fixed response GET/customers/111 { "customer": { "id": "111", "name": "John Doe", "email": "john@doe.com", "company": { "id": "222" }, "orders": [ { "id": "333" }, { "id": "444" } ] } } { "customer": { "id": "111", "name": "John Doe", "email": "john@doe.com", "company": { "href": "https://api.example.com/companies/222" }, "orders": [ { "href": "https://api.example.com/orders/333" }, { "href": "https://api.example.com/orders/444" } ] } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 7.
    REST consequences: severalroundtrips @MarcinStachniukGraphQL - when REST API is not enough - lessons learned U nderFetching
  • 8.
    REST response withnested data GET /customers/111 { "customer": { "id": "111", "name": "John Doe", "email": "john@doe.com", "company": { "id": "222", "name": "My Awesome Corporation", "website": "MyAwesomeCorporation.com" }, "orders": [ { "id": "333", "status": "delivered", "items": [ { "id": "555", "name": "Silver Bullet", "amount": "42", "price": "10000000", "currency": "USD", "producer": { "id": "777", "name": "Lorem Ipsum", "website": "LoremIpsum.com" } } ] }, { "id": "444", "name": "Golden Hammer", "amount": "5", "price": "10000", "currency": "USD", "producer": { ... } } ] } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned O verFetching
  • 9.
    REST response withnested data and limit fields GET /customers/111?fields=name,company/*,orders.status,orders.items(name,producer/name) { "customer": { "id": "111", "name": "John Doe", "email": "john@doe.com", "company": { "id": "222", "name": "My Awesome Corporation", "website": "MyAwesomeCorporation.com" }, "orders": [ { "id": "333", "status": "delivered", "items": [ { "id": "555", "name": "Silver Bullet", "amount": "42", "price": "10000000", "currency": "USD", "producer": { "id": "777", "name": "Lorem Ipsum", "website": "LoremIpsum.com" } } ] }, { "id": "444", "name": "Golden Hammer", "amount": "5", "price": "10000", "currency": "USD", "producer": { ... } } ] } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 10.
    REST response withprojection GET /customers/111?projection=customerPreview { "customer": { "id": "111", "name": "John Doe", "email": "john@doe.com", "company": { "id": "222", "name": "My Awesome Corporation", "website": "MyAwesomeCorporation.com" }, "orders": [ { "id": "333", "status": "delivered", "items": [ { "id": "555", "name": "Silver Bullet", "amount": "42", "price": "10000000", "currency": "USD", "producer": { "id": "777", "name": "Lorem Ipsum", "website": "LoremIpsum.com" } } ] }, { "id": "444", "name": "Golden Hammer", "amount": "5", "price": "10000", "currency": "USD", "producer": { ... } } ] } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 11.
    Different clients -different needs /web /iphone /android /tv Application Web iPhone Android TV RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 12.
    Different clients -different needs /web /iphone /android /tv Application Web iPhone Android TV Content-Type: application/vnd.myawesomecorporation.com+v1+web+json iphone android tv RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 13.
    Different clients -different needs /web /iphone /android /tv Application Web iPhone Android TV Content-Type: application/vnd.myawesomecorporation.com+v1+web+json iphone android tv RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 14.
    REST contract isrigid @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 15.
    Platform Architecture Platform App 1App 2 Customer App X... @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 16.
    REST API Versioning GET/v2/customers/111RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned GET /customers/111?v=v2RE ST GET /customers/111 Accept header: application/vnd.myapp.2+jsonRE ST GET /customers/111 Custom header: x-ms-version:2RE ST ● How to migrate? ● How to document? ● Is versionless API possible? ● What about continuous evolution of API?
  • 17.
    Modularisation at UI @MarcinStachniukGraphQL- when REST API is not enough - lessons learned
  • 18.
    New Frontend Framework ReactJS RelayGraphQL TypeScript @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 19.
    GraphQL ● Graph QueryLanguage ● Published by Facebook in 2015 ● Growth from Facebook Graph API ● Reference implementation in JavaScript @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 20.
    GraphQL main concepts ●One endpoint for all operations ● Always define in request what you need ● Queries, Mutations and Subscriptions ● Defined by schema @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 21.
    Data is agraph @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 22.
    GraphQL time fordemo ● Fragments ● Aliases ● Directives ● Interfaces ● Unions @MarcinStachniukGraphQL - when REST API is not enough - lessons learned ● Query ● Syntax Error ● Mutation ● Operation name ● Variables
  • 23.
    GraphQL Simple API GET/customers/2?fields=id,name,email type Customer { #fields with ! are not null id: ID! name: String! email: String! } type Query { customer(id: String!): Customer! } { "data": { "customer": { "id": "2", "name": "name", "email": "a@b.com" } } } { customer(id: "2") { id name email } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 24.
    GraphQL Bad Request GET/custo!@#$ -> 404 { "data": null, "errors": [ { "message": "Invalid Syntax", "locations": [ { "line": 2, "column": 8 } ], "errorType": "InvalidSyntax", "path": null, "extensions": null } ] } { custo!@#$ } RE ST http.cat/200 @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 25.
    Go back tothe roots @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 26.
    GraphQL Simple API GET/customers/2?fields=id,name,email,company(id,name) type Customer { id: ID! name: String! email: String! company: Company } type Company { id: ID! name: String! website: String! } type Query { customer(id: String!): Customer! } { "data": { "customer": { "id": "2", "name": "name", "email": "a@b.com", "company": { "id": "211", "name": "Company Corp." } } } } { customer(id: "2") { id name email company { id name } } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 27.
    GraphQL Simple API GET/customers/2?fields=id,name,email,orders(id,status) type Customer { id: ID! name: String! email: String! company: Company orders: [Order] } type Order { id: ID! status: Status } enum Status { NEW, CANCELED, DONE } { "data": { "customer": { "id": "2", "name": "name", "orders": [ { "id": "55", "status": "NEW" }, { "id": "66", "status": "DONE" } ] } } } { customer(id: "2") { id name orders { id status } } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 28.
    GraphQL mutations input CreateCustomerInput{ name: String email: String clientMutationId: String! } type CreateCustomerPayload { customer: Customer clientMutationId: String! } type Mutation { createCustomer(input: CreateCustomerInput): CreateCustomerPayload! } { "data": { "createCustomer": { "customer": { "id": "40", }, "clientMutationId": "123" } } } POST /customers PUT /customers/123 DELETE /customers/123 PATCH /customers/123 mutation { createCustomer(input: { name: "MyName" email: "me@me.com" clientMutationId: "123" }) { customer { id } clientMutationId } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 29.
    GraphQL mutations +operation name mutation crCust { createCustomer(input: { name: "MyName" email: "me@me.com" clientMutationId: "123" }) { customer { id } clientMutationId } } { "data": { "createCustomer": { "customer": { "id": "40", }, "clientMutationId": "123" } } } POST /customers PUT /customers/123 DELETE /customers/123 PATCH /customers/123RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned mutation { createCustomer(input: { name: "MyName" email: "me@me.com" clientMutationId: "123" }) { customer { id } clientMutationId } }
  • 30.
    GraphQL Variables { "data": { "createCustomer":{ "customer": { "id": "40", }, "clientMutationId": "123" } } } POST /customers PUT /customers/123 DELETE /customers/123 PATCH /customers/123 mutation crCust { createCustomer(input: { name: "MyName" email: "me@me.com" clientMutationId: "123" }) { customer { id } clientMutationId } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned mutation crCust ($input: CreateCustInput) { createCustomer(input: $input) { customer { id } clientMutationId } } { "input": { "name": "MyName 2", "email": "me2@me.com", "clientMutationId": "123" } }
  • 31.
    GraphQL Aliases GET /customers/2?fields=id,name,email+ { "data": { "cust1": { "id": "2", "name": "name", "email": "a@b.com" }, "cust2": { "id": "3", "name": "John Doe", "email": "john@doe.com" } } } query get2Cust { cust1: customer(id: "2") { id name email } cust2: customer(id: "3") { id name email } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned GET /customers/3?fields=id,name,emailRE ST
  • 32.
    GraphQL Fragment GET /customers/2?fields=id,name,email+ { "data": { "cust1": { "id": "2", "name": "name", "email": "a@b.com" }, "cust2": { "id": "3", "name": "John Doe", "email": "john@doe.com" } } } query get2Cust { cust1: customer(id: "2") { ... frag1 } cust2: customer(id: "3") { ... frag1 } } fragment frag1 on Customer { id name email } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned GET /customers/3?fields=id,name,emailRE ST
  • 33.
    GraphQL Directive GET /customers/2?fields=id,name { "data":{ "customer": { "id": "2", "name": "name" } } } query getCust ($showEmail: Boolean!) { customer(id: "2") { id name email @include(if: $showEmail) } } { "showEmail": false } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 34.
    GraphQL Interface GET /users?fields=id,name,superAdmin,permissions typeQuery { users: [User] } interface User { id: ID! name: String! email: String! } type Admin implements User { superAdmin: Boolean! // + id... } type Moderator implements User { permissions: [String] // + id... } { "data": { "users": [ { "id": "777", "name": "Admin a", "superAdmin": true }, { "id": "888", "name": "Moderator", "permissions": [ "Delete Customer", "Delete comment" ]}]}} query getUsers { users { ... on Admin { id name superAdmin } ... on Moderator { id name permissions } } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 35.
    GraphQL Union GET /search?input=a?fields=typename,name typeQuery { search(input: String): [SearchResult] } union SearchResult = Customer | Admin | Moderator { "data": { "search": [ { "__typename": "Customer", "name": "name" }, { "__typename": "Admin", "name": "Admin a" }, { "__typename": "Moderator", "name": "Moderator" }]}} query searchSmth { search(input: "a") { __typename ... on Customer { name } ... on Admin { name } ... on Moderator { name } } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 36.
    Q&A @MarcinStachniukGraphQL - whenREST API is not enough - lessons learned
  • 37.
    Feedback @MarcinStachniukGraphQL - whenREST API is not enough - lessons learned
  • 38.
    To be continued... @MarcinStachniukGraphQL- when REST API is not enough - lessons learned
  • 39.
    GraphQL - whenREST API is not enough - lessons learned Part 2 Marcin Stachniuk 8 October 2018
  • 40.
    Agenda 11:00 ● Why? ● Concepts ●Basics 12:00 Lunch 13:00 ● GraphQL in Java ● Lessons learned @MarcinStachniukGraphQL - when REST API is not enough - lessons learned Serverless .NET on AWS Wojciech Gawroński Lunch
  • 41.
    GraphQL ● Graph QueryLanguage ● Published by Facebook in 2015 ● Growth from Facebook Graph API ● Reference implementation in JavaScript ● First version of Java Library: 18 Jul 2015 https://github.com/graphql-java/graphql-java ● First usage: 21 Sep 2015 @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 42.
    Lessons Learned #1 Neveradd a library to your project few days after init release @MarcinStachniukGraphQL - when REST API is not enough - lessons learned ● No community ● A lot of bugs ● Bad documentation ● Strict following reference implementation and specification DO NOT TRY THIS AT WORK
  • 43.
    GraphQL Simple API GET/customers/2?fields=id,name,email,orders(id,status) type Customer { id: ID! name: String! email: String! company: Company orders: [Order] } type Order { id: ID! status: Status } enum Status { NEW, CANCELED, DONE } { "data": { "customer": { "id": "2", "name": "name", "orders": [ { "id": "55", "status": "NEW" }, { "id": "66", "status": "DONE" } ] } } } { customer(id: "2") { id name orders { id status } } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 44.
    How to implementDataFetcher for queries GET /customers/2?fields=id,name,email,orders(id,status) @Component public class CustomerFetcher extends PropertyDataFetcher<Customer> { @Autowired private CustomerService customerService; @Override public Customer get(DataFetchingEnvironment environment) { String id = environment.getArgument("id"); return customerService.getCustomerById(id); } } RE ST { customer(id: "2") { id name orders { id status } } } @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 45.
    How to implementDataFetcher for queries GET /customers/2?fields=id,name,email,orders(id,status) public class Customer { private String id; private String name; private String email; // getters are not required } RE ST { customer(id: "2") { id name orders { id status } } } @MarcinStachniukGraphQL - when REST API is not enough - lessons learned public class OrderDataFetcher extends PropertyDataFetcher<List<Order>> { @Override public List<Order> get(DataFetchingEnvironment environment) { Customer source = environment.getSource(); String customerId = source.getId(); return orderService.getOrdersByCustomerId(customerId); } }
  • 46.
    GraphQL mutations input CreateCustomerInput{ name: String email: String clientMutationId: String! } type CreateCustomerPayload { customer: Customer clientMutationId: String! } type Mutation { createCustomer(input: CreateCustomerInput): CreateCustomerPayload! } { "data": { "createCustomer": { "customer": { "id": "40", }, "clientMutationId": "123" } } } POST /customers PUT /customers/123 DELETE /customers/123 PATCH /customers/123 mutation { createCustomer(input: { name: "MyName" email: "me@me.com" clientMutationId: "123" }) { customer { id } clientMutationId } } RE ST @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 47.
    How to implementDataFetcher for mutations POST /customers PUT /customers/123 DELETE /customers/123 PATCH /customers/123 @Component public class CreateCustomersFetcher extends PropertyDataFetcher<CreateCustomersPayload> { @Override public CreateCustomerPayload get(DataFetchingEnvironment env) { Map<String, Object> input = env.getArgument("input"); String name = (String) input.get("name"); String email = (String) input.get("email"); String clientMutationId = (String) input.get("clientMutationId"); Customer customer = customerService.create(name, email); return new CreateCustomerPayload(customer, clientMutationId); } RE ST mutation { createCustomer(input: { name: "MyName" email: "me@me.com" clientMutationId: "123" }) { customer { id } clientMutationId } } @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 48.
    In case Interfacesand Unions - Type Resolver is needed public class UserTypeResolver implements TypeResolver { @Override public GraphQLObjectType getType(TypeResolutionEnvironment env) { Object javaObject = env.getObject(); if (javaObject instanceof Admin) { return env.getSchema().getObjectType("Admin"); } else if (javaObject instanceof Moderator) { return env.getSchema().getObjectType("Moderator"); } else { throw new RuntimeException("Unknown type " + javaObject.getClass().getName()); } } } public interface User {...} public class Admin implements User {...} public class Moderator implements User {...} @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 49.
    Glue everything together RuntimeWiringruntimeWiring = RuntimeWiring.newRuntimeWiring() .type("Query", builder -> builder.dataFetcher("customer", customerFetcher)) .type("Mutation", builder -> builder.dataFetcher("createCustomer", createCustomerFetcher)) .type(newTypeWiring("User") .typeResolver(new UserTypeResolver()) .build()) ... .build(); @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 50.
    Abstraction over GraphQLJava Our abstraction Data Fetcher 2 Inputs mapping to objects Schema definition Pagination ... Data Fetcher 1 Data Fetcher N... @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 51.
    Lessons Learned #2 Abstractionis not good if you don’t understand how it works under the hood @MarcinStachniukGraphQL - when REST API is not enough - lessons learned ● Copy paste errors ● Wrong usage ● Hard to update to new version
  • 52.
    GraphQL type system Howto define your schema? @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 53.
    Code First approach privateGraphQLFieldDefinition customerDefinition() { return GraphQLFieldDefinition.newFieldDefinition() .name("customer") .argument(GraphQLArgument.newArgument() .name("id") .type(new GraphQLNonNull(GraphQLString))) .type(new GraphQLNonNull(GraphQLObjectType.newObject() .name("Customer") .field(GraphQLFieldDefinition.newFieldDefinition() .name("id") .description("fields with ! are not null") .type(new GraphQLNonNull(GraphQLID)) .build()) …. .build())) .dataFetcher(customerFetcher) .build(); } Schema First approach type Query { customer(id: String!): Customer! } type Customer { #fields with ! are not null id: ID! name: String! email: String! company: Company orders: [Order] } @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 54.
    Code First approach- How to build Introspection query Introspection response Replace Relay definitions @MarcinStachniukGraphQL - when REST API is not enough - lessons learned Typescript relay plugin
  • 55.
    Schema First approach typeCustomer { # fields with ! are required id: ID! name: String! email: String! company: Company orders: [Order] } *.graphqls SchemaParser schemaParser = new SchemaParser(); File file = // ... TypeDefinitionRegistry registry = schemaParser.parse(file); SchemaGenerator schemaGenerator = new SchemaGenerator(); RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() .type("Query", builder -> builder.dataFetcher("customer", customerFetcher)) // ... .build(); return schemaGenerator.makeExecutableSchema(registry, runtimeWiring); @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 56.
    Schema First approach- project building diagram model.graphqls @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 57.
    GraphQL SPQR @MarcinStachniukGraphQL -when REST API is not enough - lessons learned https://github.com/leangen/graphql-spqr SPQR - Senatus Populusque Romanus
  • 58.
    SPQR approach public classUserService { @GraphQLQuery(name = "user") public User getById(@GraphQLArgument(name = "id") Integer id) { //... } } public class Customer { //... @GraphQLQuery(name = "name", description = "A person's name") public String getName() { return name; } } @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 59.
    Lessons Learned #3 CodeFirst Approach is the worst Schema First Approach: ● Easy to maintain and understand ● Helps organise work ● Demo schema is 3x smaller GraphQL SPQR - ? Code First approach: ● Hard to maintain ● It was the only way at the beginning to define a schema ● No possibility to mix both ● No easy way to migrate to Schema First @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 60.
    From Code Firstto Schema First migration https://github.com/mstachniuk/graphql-schema-from-introspection-generator @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 61.
    GraphQL - Howto define pagination, filtering, sorting? Pagination: ● before, after ● offset, limit Filtering: ● filter(name: “Bob” email: “%@gmail.com”) ● filter: { OR: [{ email: “%@gmail.com” }] }, name: “Bob” } Sorting: ● orderBy: ASC, DESC ● sort: NEWEST, IMPORTANCE @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 62.
    Lessons Learned #4 GraphQLis not full query language @MarcinStachniukGraphQL - when REST API is not enough - lessons learned ● Flexibility ● Less common conventions ● Dgraph.io created GraphQL+-
  • 63.
    GraphQL downsides @MarcinStachniukGraphQL -when REST API is not enough - lessons learned
  • 64.
    GraphQL downsides: N+1problem { customers { 1 call id name orders { n calls id status } } } java-dataloader ● Add async BatchLoader ● Add caching @MarcinStachniukGraphQL - when REST API is not enough - lessons learned graphQL = GraphQL.newGraphQL(schema) .queryExecutionStrategy( new BatchedExecutionStrategy()) .build(); + @Batched in DataFetcher#get()
  • 65.
    Lessons Learned #5 Ifyou have N + 1 problem use java-dataloader @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 66.
    Complex Queries fragment TypeRefon __Type { kind name ofType { kind name ofType { kind name ... } } } @MarcinStachniukGraphQL - when REST API is not enough - lessons learned graphQL = GraphQL.newGraphQL(schema) .instrumentation( new ChainedInstrumentation(asList( new MaxQueryComplexityInstrumentation(200), new MaxQueryDepthInstrumentation(20) ))) .build();
  • 67.
    Lessons Learned #6 Performancekiller? Define instrumentation! @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 68.
    GraphQL downsides: BadGraphQL API definition - examples { customer(id: "2") { … } customerFull(id: "2") { … } customerFull2(id: "2") { … } customerWithDetails(id: "2") { … } ... } @MarcinStachniukGraphQL - when REST API is not enough - lessons learned { customer(id: "2") { id name orders { id status } } }
  • 69.
    GraphQL downsides: BadGraphQL API definition - examples query getUser { user(id: "123") { ... on Admin { id superAdmin } ... on Moderator { id permissions } } } { admin(id: "123") { id superAdmin } moderator(id: "123") { id permissions } } @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 70.
    GraphQL downsides: BadGraphQL API definition - examples { orders (input: { status: "NEW" first: "2" offset: "3" }, first: "1", offset: "3") { Items { … } } @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 71.
    Lessons Learned #7 Thinkingshift is a key @MarcinStachniukGraphQL - when REST API is not enough - lessons learned ● Let’s think in graphs and NOT in endpoints / resources / entities / DTOs ● Bad design of our API
  • 72.
    GraphQL Testing @MarcinStachniukGraphQL -when REST API is not enough - lessons learned
  • 73.
    Testing GraphQL @SpringBootTest @ContextConfiguration(classes =Main) class CustomerFetcherSpec extends Specification { @Autowired GraphQLSchema graphQLSchema GraphQL graphQL def setup() { graphQL = GraphQL.newGraphQL(graphQLSchema).build() } @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 74.
    Testing GraphQL def "shouldget customer by id"() { given: def query = """{ customer(id: "2") { … } }""" def expected = [ "customer": [ … ] ] when: def result = graphQL.execute(query) then: result.data == expected } @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 75.
    Lessons Learned #8 Testingis easy @MarcinStachniukGraphQL - when REST API is not enough - lessons learned Trap Adventure 2 - "The Hardest Retro Game"
  • 76.
    Versioning @MarcinStachniukGraphQL - whenREST API is not enough - lessons learned GraphQL takes a strong opinion on avoiding versioning by providing the tools for the continuous evolution of a GraphQL schema. Src: https://graphql.org/learn/best-practices/#versioning
  • 77.
    Versioning type Query { customer(id:String!): Customer! @deprecated(reason: "not used ...") @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 78.
    Lessons Learned #9 Versioningis not a problem @MarcinStachniukGraphQL - when REST API is not enough - lessons learned GET /v2/customers/111RE ST GET /customers/111?v=v2RE ST GET /customers/111 Accept header: application/vnd.myapp.2+jsonRE ST GET /customers/111 Custom header: x-ms-version:2RE ST Versioning problem is different
  • 79.
    Tools @MarcinStachniukGraphQL - whenREST API is not enough - lessons learned
  • 80.
    Tools GraphiQL @MarcinStachniukGraphQL -when REST API is not enough - lessons learned
  • 81.
    Tools Relay @MarcinStachniukGraphQL -when REST API is not enough - lessons learned @MarcinStachniukGraphQL - when REST API is not enough - lessons learned user(...) { photo(width: "120", height: "120") } user(...) { name } user(...) { email } user(...) { name email photo(width: "120", height: "120") }
  • 82.
    Distelli/graphql-apigen https://github.com/Distelli/graphql-apigen Generate Java APIswith GraphQL Schemas in order to facilitate "schema first" development. @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 83.
    GraphQL Java Tools https://github.com/graphql-java/graphql-java-tools MapGraphQL schema to POJOs @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 84.
    More libraries andprojects related to graphql-java https://github.com/graphql-java/awesome-graphql-java @MarcinStachniukGraphQL - when REST API is not enough - lessons learned ● Generates a GraphQL schema from a JDBC data source ● Annotations-based syntax for GraphQL schema definition ● JPA Implementation of GraphQL (builds on graphql-java) ● And more examples
  • 85.
    Apollo @MarcinStachniukGraphQL - whenREST API is not enough - lessons learned Build a universal GraphQL API on top of your existing REST APIs, so you can ship new application features fast without waiting on backend changes.
  • 86.
    Lessons Learned #10 Toolingis nice @MarcinStachniukGraphQL - when REST API is not enough - lessons learned now
  • 87.
    Summary GraphQL Pros: ● Nicealternative to REST ● It can be used together with REST ● Good integration with Relay / ReactJS ● You get exactly what you want to get ● Good for API with different clients ● Good to use on top of existing API ● Self documented ● Easy testing ● Nice tooling ● Thin layer GraphQL Cons: ● High entry barrier ● Hard to return simple Map ● Not well know (yet) ● Performance overhead ● A lot of similar code to write ● No Operation Idempotency ● No Cache & Security ● No Authorisation ● Always HTTP 200 - unsupported tools @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 88.
    Nothing is asilver bullet @MarcinStachniukGraphQL - when REST API is not enough - lessons learned
  • 89.
    Q&A @MarcinStachniukGraphQL - whenREST API is not enough - lessons learned
  • 90.
    Feedback @MarcinStachniukGraphQL - whenREST API is not enough - lessons learned
  • 91.
    GraphQL - whenREST API is not enough - lessons learned Part 2 Marcin Stachniuk 8 October 2018 Thank you!