When I visualize building an application, I would think of using React and Redux on the front-end which talks to a set of RESTful services built with Node and Hapi (or Express). However, over a period of time, I've realized that this approach does not scale well when you add new features to the front-end. For example, consider a page that displays user information along with courses that a user has enrolled in. At a later point, you decide to add a section that displays popular book titles that one can view and purchase. If every entity is considered as a microservice then to get data from three different microservices would require three http requests to be sent by the front-end app. The performance of the app would degrade with the increase in the number of http requests.
I read about GraphQL and knew that it is an ideal way of building an app and I need not look forward to anything else. The GraphQL layer can be viewed as a facade which sits on top of your RESTful services or a layer which can be used to talk directly to the persistent layer. This layer provides an interface which allows its clients (front-end apps) to query the required fields from various entities in one single http request.
GraphQL - On the Server
On the server side, GraphQL provides a schema language using which you can construct the domain model. In GraphQL terms, these are `types`, a `type` is used to represent an entity such as User, Course, Book etc. It looks something like this:
type CourseType {
id: ID!
name: String
level: Int
description: String
}
Here, each field in CourseType entity has a type indicating the value that it can hold (String, Float, Character, Int etc). Apart from the Scalar types, a field could also be used to declare the relationship between two types. For example, a StudentType can have a field `courses` to declare the courses that he/she has enrolled in:
type StudentType {
id: ID!
firstName: String
lastName: String
courses: [CourseType]!
}
Here the field `courses` is of type `[CourseType]!`. The array notation is used to indicate that a Student record can have a collection of courses i.e. a student can enrol in multiple courses(1-to-many relationship). The bang (!) is used to indicate that it's a required field.
Apart from defining types, one is required to specify what can be retrieved using a `Query` and what can be updated using `Mutation`.
type Query {
allCourses: [CourseType]
allStudents: [StudentType]
}
type Mutation {
createCourse(name: String, level: Int, description: String): CourseType
createStudent(firstName: String, lastName: String, courses: [CourseType]!): StudentType
updateCourse(id: ID!, name: String, level: Int, description: String): CourseType
.
.
.
}
Here types `Query` and `Mutation` are special types. They're not used to define an entity, rather they're used to specify the operations that you can perform on these entities. For example, `allCourses` in `Query` is used to declare that the client can use this to retrieve a list of courses (returns [CourseType]). A Mutation type, on the other hand, is used to indicate the operations that you can perform to update these entities. In the above example, createCourse, createStudent etc are the operations that you can perform to add or update records.
All the above types put together come to form the type definition for your schema and it gives a high-level overview of the domain model and the operations that you could perform on various entities. However, the business logic for how the data would be retrieved or updated is not present. To accomplish this, one is required to write resolvers. A resolver provides the implementation that maps to the operations declared in the schema. A sample resolver for the above example looks like this:
const resolvers = {
Query: {
allCourses: () => {
return COURSES;
}
},
Mutation: {
createCourse: (_, { input }) => {
let { id, name, description, level } = input;
COURSES.push({
id,
name,
description,
level
});
return input;
}
}
};
The resolvers object contains two properties Query and Mutation; under these, you would define the various operations that can be performed on the entities declared in the schema. Note, that the name of the operations would map to the ones specified in the schema (here allCourses in type Query and createCourse in type Mutation). The type definitions are independent of the language or the platform that you choose. However, the resolvers are written in the language of your choice; in this case JavaScript.
The type definitions and resolvers come together to define the executable schema which can then be used to define middleware in a Node + Express environment:
const schema = makeExecutableSchema({ typeDefs, resolvers});
app.use('/graphql',
bodyParser.json(),
graphqlExpress({
schema
}));
I've used `graphql-tools` and `apollo-graphql-express` modules to create the schema and the middleware which would accept incoming requests.
To test the server setup, you could make use of the GraphiQL interface which allows you to inspect the operations declared in the schema, add this:
app.use('/graphiql',
graphiqlExpress({
endpointURL: '/graphql'
}));
The graphiql interface provides us with a way to test the operations declared in `Query` and `Mutation` of our schema:
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import Course from './Course';
class CourseList extends Component {
render() {
const { allCourses } = this.props.data;
if (!allCourses) return null;
return allCourses.map(course => {
return (
<Course key={course.id}
course={course} />
);
});
}
}
export default graphql(gql`
query CourseListQuery {
allCourses {
id
name
description
}
}
`)(CourseList);
Here the graphql higher-order function accepts two arguments - the GraphQL query and the view component that has to be updated with the query result. The result of the query response is passed to the component as props (this.props.data).
Next steps:
I've briefly read about `Relay` and I plan to experiment with it to see how it can be used to connect to a GraphQL server and compare it to see how it fairs with Apollo.
GitHub repo - https://github.com/sagar-ganatra/react-graphql-apollo-app
I read about GraphQL and knew that it is an ideal way of building an app and I need not look forward to anything else. The GraphQL layer can be viewed as a facade which sits on top of your RESTful services or a layer which can be used to talk directly to the persistent layer. This layer provides an interface which allows its clients (front-end apps) to query the required fields from various entities in one single http request.
GraphQL - On the Server
On the server side, GraphQL provides a schema language using which you can construct the domain model. In GraphQL terms, these are `types`, a `type` is used to represent an entity such as User, Course, Book etc. It looks something like this:
type CourseType {
id: ID!
name: String
level: Int
description: String
}
Here, each field in CourseType entity has a type indicating the value that it can hold (String, Float, Character, Int etc). Apart from the Scalar types, a field could also be used to declare the relationship between two types. For example, a StudentType can have a field `courses` to declare the courses that he/she has enrolled in:
type StudentType {
id: ID!
firstName: String
lastName: String
courses: [CourseType]!
}
Here the field `courses` is of type `[CourseType]!`. The array notation is used to indicate that a Student record can have a collection of courses i.e. a student can enrol in multiple courses(1-to-many relationship). The bang (!) is used to indicate that it's a required field.
Apart from defining types, one is required to specify what can be retrieved using a `Query` and what can be updated using `Mutation`.
type Query {
allCourses: [CourseType]
allStudents: [StudentType]
}
type Mutation {
createCourse(name: String, level: Int, description: String): CourseType
createStudent(firstName: String, lastName: String, courses: [CourseType]!): StudentType
updateCourse(id: ID!, name: String, level: Int, description: String): CourseType
.
.
.
}
Here types `Query` and `Mutation` are special types. They're not used to define an entity, rather they're used to specify the operations that you can perform on these entities. For example, `allCourses` in `Query` is used to declare that the client can use this to retrieve a list of courses (returns [CourseType]). A Mutation type, on the other hand, is used to indicate the operations that you can perform to update these entities. In the above example, createCourse, createStudent etc are the operations that you can perform to add or update records.
All the above types put together come to form the type definition for your schema and it gives a high-level overview of the domain model and the operations that you could perform on various entities. However, the business logic for how the data would be retrieved or updated is not present. To accomplish this, one is required to write resolvers. A resolver provides the implementation that maps to the operations declared in the schema. A sample resolver for the above example looks like this:
const resolvers = {
Query: {
allCourses: () => {
return COURSES;
}
},
Mutation: {
createCourse: (_, { input }) => {
let { id, name, description, level } = input;
COURSES.push({
id,
name,
description,
level
});
return input;
}
}
};
The resolvers object contains two properties Query and Mutation; under these, you would define the various operations that can be performed on the entities declared in the schema. Note, that the name of the operations would map to the ones specified in the schema (here allCourses in type Query and createCourse in type Mutation). The type definitions are independent of the language or the platform that you choose. However, the resolvers are written in the language of your choice; in this case JavaScript.
The type definitions and resolvers come together to define the executable schema which can then be used to define middleware in a Node + Express environment:
const schema = makeExecutableSchema({ typeDefs, resolvers});
app.use('/graphql',
bodyParser.json(),
graphqlExpress({
schema
}));
I've used `graphql-tools` and `apollo-graphql-express` modules to create the schema and the middleware which would accept incoming requests.
To test the server setup, you could make use of the GraphiQL interface which allows you to inspect the operations declared in the schema, add this:
app.use('/graphiql',
graphiqlExpress({
endpointURL: '/graphql'
}));
The graphiql interface provides us with a way to test the operations declared in `Query` and `Mutation` of our schema:
Here, the object notation on the left side allows me to specify the query method and the fields that I need from the `courses` model (id and name). The response from the server (on the right side) includes only the data that I have requested. Although the resolver itself returns the entire object, GraphQL ensures that only the requested data is returned in the response; thus reducing the size of the payload.
GraphQL - On the client side
On the client side, the first step would be to connect to the GraphQL server and wrap the root component with the Provider (ApolloProvider from `react-apollo`):
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from 'react-apollo';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import './index.css';
import App from './App';
const client = new ApolloClient({
link: new HttpLink({ uri: 'http://localhost:8080/graphql' })
})
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root'));
Next, we use the `react-apollo` module which provides a higher-order function using which we can glue together a component with the result from a GraphQL query response:
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import Course from './Course';
class CourseList extends Component {
render() {
const { allCourses } = this.props.data;
if (!allCourses) return null;
return allCourses.map(course => {
return (
<Course key={course.id}
course={course} />
);
});
}
}
export default graphql(gql`
query CourseListQuery {
allCourses {
id
name
description
}
}
`)(CourseList);
Here the graphql higher-order function accepts two arguments - the GraphQL query and the view component that has to be updated with the query result. The result of the query response is passed to the component as props (this.props.data).
Next steps:
I've briefly read about `Relay` and I plan to experiment with it to see how it can be used to connect to a GraphQL server and compare it to see how it fairs with Apollo.
GitHub repo - https://github.com/sagar-ganatra/react-graphql-apollo-app
Comments
Post a Comment