GraphQL is the alternative for REST made by Facebook. When facebook reached the limits of REST API, they had to make something breaking these limits. Through this article, you can learn what GraphQL is, how it works, and how to use it in your projects.
GraphQL (Graph Query Language) is an API-level query language. Like SQL is used for requests to relational databases, GraphQL allows clients to request data from an API.
For a good understanding of this article, you should have a basic understanding of TypeScript and NodeJS.
GraphQL allows you to manage what data API will return to you. This has a number of advantages. It helps network performance when the response contains a lot of data you don't need. Of course, REST can handle it via request parameters like ?fields=name,birthdate
, however much easier when it is available by default, does it? It is also useful if your API supports different types of clients which want to call less and fetch more.
GraphQL allows you to get a complex graph of objects by a single request. These objects might be from different sources.
Your clients and server have the same scheme, so it is easy to sync it.
To understand how GraphQL realizes all you’ve read above, let’s talk about what artifacts GraphQL includes.
GraphQL includes schema, which describes the types of objects you can operate on. Take a look at the following schema:
type App {
id: ID!
name: String!
}
This is the type App, which includes id and name required fields. It means you can operate this object. But how to operate it? There are a few access operators in GraphQL. The first one (required one) - query. This is a getter for your object. The second one - mutation, is a setter. And the last one is a subscription, which allows you to subscribe for updates of your type.
type Query {
apps: [App!]!
app(id: ID!): App!
}
type Mutation {
createApp(app: AppInput!): App!
updateApp(appId: ID!, app: AppInput!): App!
}
input AppInput {
name: String!
publisherId: ID!
}
type Subscription {
appMutated: AppMutationPayload!
}
type AppMutationPayload {
mutation: MutationType!
node: App!
}
enum MutationType {
CREATED
UPDATED
DELETED
}
The code snippet above contains input AppInput
. Inputs allow you to define input parameters types.
Another important component of GraphQL - resolvers. Resolvers define how to handle queries, mutations, and subscribers. We will come back to it soon.
Apollo Server is a Node.js implementation of graphQL server. There is good scalable architecture and a strong community that means it is a good choice for use. We won’t talk much about Apollo architecture, we’ll learn everything needed by code examples.
"dependencies": {
"apollo-datasource": "^0.8.0",
"apollo-server": "^2.22.1",
"graphql": "^15.5.0"
},
"devDependencies": {
"@types/graphql": "^14.5.0",
"@types/node": "^14.14.37",
"ts-node": "^9.1.1",
"typescript": "^4.2.3"
}
apollo-server
- allows implementing GraphQL server apollo-datasource
- allows implementing data sources (like database aggregators or rest api integrations) graphql
- graphQL package. Required by Apollo.
import {ApolloServer, gql} from 'apollo-server'
import {typeDefs} from './graphql/typedefs'
import {resolvers} from './graphql/resolvers'
import {dataSources} from './datasources'
const server = new ApolloServer({
typeDefs: gql`
${typeDefs}
`,
resolvers,
dataSources
})
server.listen().then(({url}) => {
console.log(`Server ready at ${url}`)
})
Let’s understand the code above. We are initializing Apollo Server with several arguments.
typeDefs
- this is a path for graphQL schema
resolvers
- as a said earlier, we will come back for that a bit later
dataSources
- data sources like DB aggregators and API integrations
import { AppService } from './app-service';
export const dataSources = () => ({
appService: new AppService()
});
This is DI of dataSources. This format is required by Apollo.
import { DataSource } from 'apollo-datasource';
export class AppService extends DataSource {
constructor() {
super();
}
}
dataSource might look like this. This is an aggregator, where you can implement business logic and accessors for data.
Finally, let’s talk about resolvers. As I said earlier, resolvers define how to handle queries, mutations, and subscribers. Take a look at that:
type Mutation {
createApp(app: AppInput!): App!
updateApp(appId: ID!, app: AppInput!): App!
}
type Subscription {
appMutated: AppMutationPayload!
}
type AppMutationPayload {
mutation: MutationType!
node: App!
}
And now take a look at the resolver:
import { pubsub } from '../pubsub';
const APP_MUTATED = 'appMutated';
export default {
Query: {
apps(parent, args, {dataSources}) {
return dataSources.appService.getApps();
},
app(parent, args, {dataSources}) {
return dataSources.appService.getApp(args.id);
}
},
Mutation: {
async createApp(parent, args, {dataSources}) {
const { publisherId, ...rest } = args.app;
let app = await dataSources.appService.createApp({...rest}, publisherId);
pubsub.publish(APP_MUTATED, {
appMutated: {
mutation: 'CREATED',
node: app
}
});
return app;
},
async updateApp(parent, args, {dataSources}) {
const { publisherId, ...rest } = args.app;
let updatedApp = await dataSources.appService.updateApp(args.appId, {...rest}, publisherId);
publishAppUpdated(updatedApp);
return updatedApp;
},
async setAppDevelopers(parent, args, {dataSources}) {
let updatedApp = await dataSources.appService.setAppDevelopers(args.appId, args.developerIds)
publishAppUpdated(updatedApp);
return updatedApp;
}
},
Subscription: {
appMutated: {
subscribe: () => pubsub.asyncIterator(APP_MUTATED)
}
},
};
function publishAppUpdated(app) {
pubsub.publish(APP_MUTATED, {
appMutated: {
mutation: 'UPDATED',
node: app
}
});
return app;
}
This resolver totally defines all queries, mutations, and subscribers. And as you can see, we have access to data sources that were injected into ApolloServer.
let app = await dataSources.appService.createApp({...rest}
We deal with this, but what about subscribers?
Subscription: {
appMutated: {
subscribe: () => pubsub.asyncIterator(APP_MUTATED)
}
}
And what is the code in mutations?
pubsub.publish(APP_MUTATED, {
appMutated: {
mutation: 'CREATED',
node: app
}
});
This is pubSub. Maybe you know this pattern. Microservices usually communicate using it. This is required for subscribers in Apollo. And it makes sense because subscribers are WebSockets. Websockets keep connections, so it is easy to reach a high load. So, the Apollo team decided to handle it with this design solution. Anyway, there is an in-memory dummy, just for testing. Never use it in production.
import { PubSub } from 'apollo-server';
export const pubsub = new PubSub();
Run the app using the following command:
ts-node src/index.ts
Go to localhost:4000 via web browser and see the following:
This is the test console Apollo. You can test queries, mutations, and subscriptions here.
Congratulations! You’ve just learned how to build graphQL API using Apollo and Node.JS.
Full code example you can find here: https://github.com/gandronchik/graphql-example