Deep Dive into Apollo GraphQL Http Server
A Series Blog for Deep Dive into Apollo GraphQL from Backend to Frontend
This is a 7th of the series blogs on deep dive into Apollo GraphQL from backend to frontend. A lot of information is Apollo GraphQL Doc or GraphQL Doc as well as their source code on Github — all tributes go to them. For my part, I would like to give you my “destructuring” of the original knowledge and my reflection on it, analysis on the code examples/source code as well as some extra examples.
In this blog, we are going to take a look at the how Http server is glued with Apollo Server, and how we can can leverage Http Link in the Apollo Client to build a middleware pipeline.
GraphQL over Http
Web Request Pipeline: Most modern web frameworks use a pipeline model where requests are passed through a stack of middleware. As the request flows through the pipeline, it can be inspected, transformed, modified, or terminated with a response. GraphQL should be placed after all authentication middleware, so that you have access to the same session and user information you would in your HTTP endpoint handlers.
Apollo Server accepts both GET and POST requests.
Post: Apollo Server accepts POST requests with a JSON body. A valid request must contain either a query
or an operationName
, and may include variables
:
{
"query": "query aTest($arg1: String!) { test(who: $arg1) }",
"operationName": "aTest",
"variables": { "arg1": "me" }
}
Batching: a batch of queries can be sent by simply sending a JSON-encoded array of queries, e.g.[{ query: '{ testString }' }, { query: 'query q2{ test(who: "you" ) }' }];
. In this case, the response will be an array of GraphQL responses.
Get: Apollo Server also accepts GET requests. A GET request must pass query and optionally variables and operationName in the URL:
GET /graphql?query=query%20aTest(%24arg1%3A%20String!)%20%7B%20test(who%3A%20%24arg1)%20%7D&operationName=aTest&variables=me
Note that Mutations cannot be executed via GET requests.
Response: Regardless of the method by which the query is sent, the response should be returned in the body of the request in JSON format:
{ "data": { ... }, "errors": [ ... ]}
Apollo Server bridging
HTTP and GraphQL.js
As discussed in my earlier blogs, the apollo-server
is really just a thin layer between HTTP server (which uses express.js
) and and GraphQL.
- Make sure that GraphQL query contained in the body of an incoming POST request can be executed by GraphQL.js by parsing out the query and forwarding it to the
graphql
function for execution. - Attach the result of the execution to the response object so it can be returned to the client over Http.
Links in Apollo Client
According to the Doc, the Apollo Link library helps you customise the flow of data between Apollo Client and your GraphQL server. You can define your client’s network behaviour as a chain of link objects that execute in a sequence. So each link operates on the result of the previous link. This allows to “compose” actions in a pipeline:
For example:
By default, Apollo Client uses Apollo Link’s HttpLink
to send GraphQL operations to a remote server over HTTP.
import { ApolloClient, InMemoryCache, HttpLink, from } from "@apollo/client";
import { onError } from "@apollo/client/link/error";const httpLink = new HttpLink({
uri: "http://localhost:4000/graphql"
});const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache()
});
So what is a link? At a basic level, a link is a function that takes an operation and returns an observable. And what is an operation? Well, an object with the following properties:
- query: DocumentNode (parsed GraphQL Operation) describing the operation taking place.
- variables: variables sent with the operation.
- operationName: string name of the query if it is named
- extensions: a map to store extensions data to be sent to the server.
- getContext: a function to return the context of the request. This context can be used by links to determine which actions to perform.
- setContext: afunction that takes either a new context object, or a function which receives the previous context and returns a new one. It behaves similarly to setState from React.
And looking at the Type Definitions:
export declare type RequestHandler = (operation: Operation, forward: NextLink) => Observable<FetchResult> | null;export declare type NextLink = (operation: Operation) => Observable<FetchResult>;export interface GraphQLRequest {
query: DocumentNode;
variables?: Record<string, any>;
operationName?: string;
context?: Record<string, any>;
extensions?: Record<string, any>;
}export interface Operation {
query: DocumentNode;
variables: Record<string, any>;
operationName: string;
extensions: Record<string, any>;
setContext: (context: Record<string, any>) => Record<string, any>;
getContext: () => Record<string, any>;
}export interface FetchResult<TData = {
[key: string]: any;
}, C = Record<string, any>, E = Record<string, any>> extends ExecutionResult {
data?: TData | null;
extensions?: E;
context?: C;
}
At the core of a link is the request method. The request method is called every time execute is run on that link chain, and the link receives an operation and return back data of some kind in the form of an Observable.
export declare type RequestHandler = (operation: Operation, forward: NextLink) => Observable<FetchResult> | null;
- operation: The operation being passed through the link.
- forward: (optional) Specifies the next link in the chain of links.
When your custom link’s request handler is done its part, it should return a call to the forward
like return forward(operation)
and this passes execution to the next link in the chain(unless it’s the chain’s terminating link).
Since we talk about pipeline, let’s take a look at three examples that happen in different phases at the request/response lifecycle.
- Request — Middleware
Middlewares can intercept every request made over the link, for example, adding authentication tokens.
const httpLink = new HttpLink({ uri: '/graphql' });const authMiddleware = new ApolloLink((operation, forward) => {
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: localStorage.getItem('token') || null,
}
}));return forward(operation);
})const activityMiddleware = new ApolloLink((operation, forward) => {
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
'recent-activity': localStorage.getItem('lastOnlineTime') || null,
}
}));return forward(operation);
})const client = new ApolloClient({
cache: new InMemoryCache(),
link: from([
authMiddleware,
activityMiddleware,
httpLink
]),
});
- Response— Afterware
Afterware is like middleware, except that an afterware runs when a response is to be sent. For example, you can operate on the response.data
by map
on the result of the link's forward(operation)
call.
import { ApolloClient, InMemoryCache, HttpLink, ApolloLink } from '@apollo/client';const httpLink = new HttpLink({ uri: '/graphql' });const addDateLink = new ApolloLink((operation, forward) => {
return forward(operation).map(response => {
response.data.date = new Date();
return response;
});
});const client = new ApolloClient({
cache: new InMemoryCache(),
link: addDateLink.concat(httpLink),
});
Context on Apollo Link Chain
Since we know Http Links can chain together, essentially we can use it as middleware from side effects like logging, error handling, or even network requests.
To have a shared context that can help each link gets the information, each Operation has a context object which can be set from the operation, through operation.getContext()
and operation.setContext()
. This context is not sent to the server, but is used for communications between links. As an example:
const timeStartLink = new ApolloLink((operation, forward) => {
operation.setContext({ start: new Date() });
return forward(operation);
});const logTimeLink = new ApolloLink((operation, forward) => {
return forward(operation).map((data) => {
const time = new Date() — operation.getContext().start;
console.log(`operation ${operation.operationName} took ${time} to complete`);
return data;
})
});
const link = timeStartLink.concat(logTimeLink)
Note that you can also set context on the operation itself:
const query = client.query({ query: EXAMPLE_QUERY, context: { someContext: true }});
Compose a Link Chain
ApolloLink Class has a lot of useful methods at you disposal to help build a link chain.
export declare class ApolloLink {
static empty(): ApolloLink;
static from(links: (ApolloLink | RequestHandler)[]): ApolloLink;
static split(test: (op: Operation) => boolean, left: ApolloLink | RequestHandler, right?: ApolloLink | RequestHandler): ApolloLink;
static execute(link: ApolloLink, operation: GraphQLRequest): Observable<FetchResult>;
static concat(first: ApolloLink | RequestHandler, second: ApolloLink | RequestHandler): ApolloLink;
constructor(request?: RequestHandler);
split(test: (op: Operation) => boolean, left: ApolloLink | RequestHandler, right?: ApolloLink | RequestHandler): ApolloLink;
concat(next: ApolloLink | RequestHandler): ApolloLink;
request(operation: Operation, forward?: NextLink): Observable<FetchResult> | null;
protected onError(error: any, observer?: ZenObservable.Observer<FetchResult>): false | void;
setOnError(fn: ApolloLink["onError"]): this;
}
There are two compositions: additive and directional.
- Additive composition is how you can combine multiple links into a single chain and,
- Directional composition is how you can control which links are used depending on the operation.
Additive Composition
To do additive composition, you use from
or concat
. From takes an array of links and combines them all into a single link, concat joins two links together into one.
import { ApolloLink } from 'apollo-link';
import { RetryLink } from 'apollo-link-retry';
import { HttpLink } from 'apollo-link-http';
import MyAuthLink from '../auth';const link = ApolloLink.from([
new RetryLink(),
new MyAuthLink(),
new HttpLink({ uri: 'http://localhost:4000/graphql' })
]);const link = ApolloLink.concat(new RetryLink(), new HttpLink({ uri: 'http://localhost:4000/graphql' }));
Directional Composition
This way of composition introduces logical flow so you can decide the link to use based on certain condition, e.g. depending on the operation itself or any other global state. This is done using the split
method:
import {
ApolloClient,
ApolloLink,
split,
} from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { WebSocketLink } from '@apollo/client/link/ws';
const DEV_ENDPOINT = 'http://localhost:6688/graphql';
const DEV_WS_ENDPOINT = 'ws://localhost:6688/graphql';
let apolloClient: ApolloClient<NormalizedCacheObject> | null = null;
const httpLink = createHttpLink({
uri: process.env.NODE_ENV === 'development' ? DEV_ENDPOINT : process.env.NEXT_PUBLIC_PROD_ENDPOINT,
fetchOptions: {
credentials: 'include',`credentials` or `headers`,
},
});
const wsLink = process.browser
? new WebSocketLink({
uri:
process.env.NODE_ENV === 'development'
? (DEV_WS_ENDPOINT as string)
: (process.env.NEXT_PUBLIC_PROD_WS_ENDPOINT as string),
options: {
reconnect: true,
minTimeout: 10000,
timeout: 30000,
lazy: true,
},
})
: null;
const thirdPartyLink = createHttpLink({
uri: 'https://api.spacex.land/graphql/',
});
const spaceXLink = process.browser
? split(
({ query }) => {
const definition = getMainDefinition(query);
return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
},
wsLink as ApolloLink,
httpLink
)
: httpLink;
const terminalLink = split((op) => op.getContext().clientName === 'thirdParty', thirdPartyLink, spaceXLink);
function createApolloClient() {
return new ApolloClient({
link: terminalLink,
cache: cache,
});
}
Http Link
HTTP Link takes an options
object to customise the behaviour of the link:
export declare class HttpLink extends ApolloLink {
options: HttpOptions;
requester: RequestHandler;
constructor(options?: HttpOptions);
}export interface HttpOptions {
uri?: string | UriFunction;
includeExtensions?: boolean;
fetch?: WindowOrWorkerGlobalScope['fetch'];
headers?: any;
credentials?: string;
fetchOptions?: any;
useGETForQueries?: boolean;
includeUnusedVariables?: boolean;
}
- uri: the URI key is a string endpoint or function resolving to an endpoint — default to “/graphql” if not specified
- includeExtensions: allow passing the extensions field to your graphql server, defaults to false
- fetch: a fetch compatible API for making a request
- headers: an object representing values to be sent as headers on the request
- credentials: a string representing the credentials policy you want for the fetch call. Possible values are: omit, include and same-origin
- fetchOptions: any overrides of the fetch options argument to pass to the fetch call
- useGETForQueries: set to true to use the HTTP GET method for queries
HttpLink
checks the current operation's context
that we discussed earlier for certain values before sending its request to your GraphQL endpoint. For example:
const client = new ApolloClient({
link: createHttpLink({ uri: "/graphql" }),
cache: new InMemoryCache()
});
client.query({
query: MY_QUERY,
context: {
headers: {
special: "Special header value"
}
}
});
createHttpLink vs new HttpLink()
You can create the HttpLink by createHttpLink
and new HttpLink()
. There is no fundamental difference between the two. If you look at the apollo-link-http
package source here, you can see that the createHttpLink
method returns a new instance of the ApolloLink
class initialized with the options you passed to createHttpLink
.
At the end of the same file, you can see that the package also exports the HttpLink
class extends the ApolloLink
class.
As a result, when you create new instance of the HttpLink
class, the options you pass to the constructor are passed down to createHttpLink
, which is an instance of ApolloLink
. However since apollo-link-http
package's own docs exports the createHttpLink
, we might as well use it than new HttpLink
.
That’s so much of it!
Happy Reading!