Meteor

Specifics about using Apollo in your Meteor application.

The Apollo client and server tools are published on npm, which makes them available to all JavaScript applications, including those written with Meteor 1.3 and above. When using Meteor with Apollo, you can use those npm packages directly, or you can use the apollo Atmosphere package, which simplifies things for you.

To install apollo, run these commands:

1
2
meteor add apollo
meteor npm install --save apollo-client graphql-server-express express graphql graphql-tools body-parser

Usage

Examples

You can see this package in action in the Apollo Meteor starter kit.

If you’d like to understand how this simple package works internally, you are invited to take the code tour.

Client

Connect to the Apollo server with meteorClientConfig:

1
2
3
4
import ApolloClient from 'apollo-client';
import { meteorClientConfig } from 'meteor/apollo';
const client = new ApolloClient(meteorClientConfig());

Server

Create the following files:

1
2
/imports/api/schema.js # a JavaScript file with the schema
/imports/api/resolvers.js # a JavaScript file with the Apollo resolvers

Define a simple schema under schema.js.

1
2
3
4
5
6
export const typeDefs = `
type Query {
say: String
}
`;

Define your first resolver under resolvers.js.

1
2
3
4
5
6
7
export const resolvers = {
Query: {
say(root, args, context) {
return 'hello world';
}
}
}

Set up the Apollo server with createApolloServer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createApolloServer } from 'meteor/apollo';
import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools';
import { typeDefs } from '/imports/api/schema';
import { resolvers } from '/imports/api/resolvers';
const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
createApolloServer({
schema,
});

The GraphiQL url by default is http://localhost:3000/graphiql. You can now test your first query:

1
2
3
{
say
}

Inside your resolvers, if the user is logged in, their id will be context.userId and their user doc will be context.user:

1
2
3
4
5
6
7
8
9
10
11
export const resolvers = {
Query: {
user(root, args, context) {
// Only return the current user, for security
if (context.userId === args.id) {
return context.user;
}
},
},
User: ...
}

Query batching

meteor/apollo gives you a BatchedNetworkInterface by default thanks to createMeteorNetworkInterface. This interface is meant to reduce significantly the number of requests sent to the server.

In order to get the most out of it, you can attach a dataloader to every request to batch loading your queries (and cache them!).

Here are some great resources to help you integrating query batching in your Meteor application:

Deployment

It is strongly recommended to explictly specify the ROOT_URL environment variable of your deployment. The configuration of the Apollo client and GraphQL server provided by this package depends on a configured ROOT_URL. Read more about that in the Meteor Guide.

API

meteorClientConfig

meteorClientConfig(customClientConfig = {})

The customClientConfig is an optional object that can have any Apollo Client options.

Defining a customClientConfig object extends or replaces fields of the default configuration provided by the package.

The default configuration of the client is:

  • networkInterface: createMeteorNetworkInterface(), a pre-configured network interface. See below for more information.
  • ssrMode: Meteor.isServer, enable server-side rendering mode by default if used server-side.

The store is normalized by default with __typename + _id identifiers. See store normalization section for more information.

createMeteorNetworkInterface

createMeteorNetworkInterface(customNetworkInterface = {})

customNetworkInterface is an optional object that replaces fields of the default configuration:

  • uri: Meteor.absoluteUrl('graphql'), points to the default GraphQL server endpoint, such as http://locahost:3000/graphql or https://www.my-app.com/graphql.
  • opts: {}, additional FetchOptions passed to the NetworkInterface.
  • useMeteorAccounts: true, enable the Meteor User Accounts middleware to identify the user with every request thanks to her login token.
  • batchingInterface: true, use a BatchedNetworkInterface by default instead of NetworkInterface.
  • batchInterval: 10, if the batchingInterface field is true, this field defines the batch interval to determine how long the network interface batches up queries before sending them to the server.

Additionally, if the useMeteorAccounts is set to true, you can add to your customNetworkInterface a loginToken field while doing server-side rendering to handle the current user.

createMeteorNetworkInterface example:

1
2
3
4
5
6
7
8
9
import ApolloClient from 'apollo-client'
import { createMeteorNetworkInterface, meteorClientConfig } from 'meteor/apollo';
const networkInterface = createMeteorNetworkInterface({
// use a batched network interface instead of a classic network interface
batchingInterface: true,
});
const client = new ApolloClient(meteorClientConfig({ networkInterface }));

createApolloServer

createApolloServer(customOptions = {}, customConfig = {})

createApolloServer is used to create and configure an Express GraphQL server.

customOptions is an object that can have any GraphQL Server options, used to enhance the GraphQL server run thanks to graphqlExpress.

Defining a customOptions object extends or replaces fields of the default configuration provided by the package:

  • context: {}, ensure that a context object is defined for the resolvers.
  • formatError: a function used to format errors before returning them to clients.
  • debug: Meteor.isDevelopment, additional debug logging if execution errors occur in dev mode.

It is on customOptions object that you pass a schema field created by makeExecutableSchema (see usage).

customConfig is an optional object that can be used to replace the configuration of how the Express server itself runs:

  • path: path of the GraphQL server. This is the endpoint where the queries & mutations are sent. Default: /graphql.
  • configServer: a function that is given to the express server for further configuration. You can for instance enable CORS with createApolloServer({}, {configServer: expressServer => expressServer.use(cors())})
  • graphiql: whether to enable GraphiQL. Default: true in development and false in production.
  • graphiqlPath: path for GraphiQL. Default: /graphiql (note the i).
  • graphiqlOptions: GraphiQL options Default: attempts to use Meteor.loginToken from localStorage to log you in.

It will use the same port as your Meteor server. Don’t put a route or static asset at the same path as the GraphQL route or the GraphiQL route if in use (again, defaults are /graphql and /graphiql respectively).

Accounts

You may still use the authentication based on DDP (Meteor’s default data layer) and apollo will send the current user’s login token to the GraphQL server with each request.

If you want to use only GraphQL in your app you can use nicolaslopezj:apollo-accounts. This package uses the Meteor Accounts methods in GraphQL, it’s compatible with the accounts you have saved in your database and you may use nicolaslopezj:apollo-accounts and Meteor’s DDP accounts at the same time.

If you are relying on the current user in your queries, you’ll want to clear the store when the current user state changes. To do so, use client.resetStore() in the Meteor.logout callback:

1
2
3
4
5
6
7
// The `client` variable refers to your `ApolloClient` instance.
// It would be imported in your template,
// or passed via props thanks to `withApollo` in React for example.
Meteor.logout(function() {
return client.resetStore(); // make all active queries re-run when the log-out process completed
});

SSR

There are two additional configurations that you need to keep in mind when using React Server Side Rendering with Meteor.

  1. Use isomorphic-fetch to polyfill fetch server-side (used by Apollo Client’s network interface).
  2. Connect your express server to Meteor’s existing server with WebApp.connectHandlers.use
  3. Do not end the connection with res.send() and res.end() use req.dynamicBody and req.dynamicHead instead and call next(). more info

The idea is that you need to let Meteor to finally render the html you can just provide it extra body and or head for the html and Meteor will append it, otherwise CSS/JS and or other merged html content that Meteor serve by default (including your application main .js file) will be missing.

Here is a full working example:

1
2
meteor add apollo webapp
meteor npm install --save react react-dom apollo-client redux react-apollo react-router react-helmet express isomorphic-fetch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { meteorClientConfig } from 'meteor/apollo';
import React from 'react';
import ReactDOM from 'react-dom/server';
import ApolloClient, { createNetworkInterface } from 'apollo-client';
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import { ApolloProvider, renderToStringWithData } from 'react-apollo';
import { match, RouterContext } from 'react-router';
import Express from 'express';
// #1 import isomorphic-fetch so the network interface can be created
import 'isomorphic-fetch';
import Helmet from 'react-helmet';
import routes from '../both/routes';
import rootReducer from '../../ui/reducers';
import Body from '../both/routes/body';
// 1# do not use new
const app = Express(); // eslint-disable-line new-cap
app.use((req, res, next) => {
match({ routes, location: req.originalUrl }, (error, redirectLocation, renderProps) => {
if (redirectLocation) {
res.redirect(redirectLocation.pathname + redirectLocation.search);
} else if (error) {
console.error('ROUTER ERROR:', error); // eslint-disable-line no-console
res.status(500);
} else if (renderProps) {
// use createMeteorNetworkInterface to get a preconfigured network interface
// #1 network interface can be used server-side thanks to polyfilled `fetch`
const networkInterface = createMeteorNetworkInterface({
opts: {
credentials: 'same-origin',
headers: req.headers,
},
// possible current user login token stored in the cookies thanks to
// a third-party package like meteorhacks:fast-render
loginToken: req.cookies['meteor-login-token'],
});
// use meteorClientConfig to get a preconfigured Apollo Client options object
const client = new ApolloClient(meteorClientConfig({ networkInterface }));
const store = createStore(
combineReducers({
...rootReducer,
apollo: client.reducer(),
}),
{}, // initial state
compose(
applyMiddleware(client.middleware()),
),
);
const component = (
<ApolloProvider store={store} client={client}>
<RouterContext {...renderProps} />
</ApolloProvider>
);
renderToStringWithData(component).then((content) => {
const initialState = client.store.getState()[client.reduxRootKey].data;
// the body content we want to append
const body = <Body content={content} state={initialState} />;
// #3 `req.dynamicBody` will hold that body and meteor will take care of
// actually appending it to the end result
req.dynamicBody = ReactDOM.renderToStaticMarkup(body);
const head = Helmet.rewind();
// #3 `req.dynamicHead` in this case we use `react-helmet` to add seo tags
req.dynamicHead = ` ${head.title.toString()}
${head.meta.toString()}
${head.link.toString()}
`;
// #3 Important we do not want to return this, we just let meteor handle it
next();
});
} else {
console.log('not found'); // eslint-disable-line no-console
}
});
});
// #2 connect your express server with meteor's
WebApp.connectHandlers.use(Meteor.bindEnvironment(app));

Apollo Optics

Here’s a minimal example of Apollo Optics integration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createApolloServer } from 'meteor/apollo';
import OpticsAgent from 'optics-agent';
import executableSchema from 'schema.js';
OpticsAgent.instrumentSchema(executableSchema);
createApolloServer(req => ({
schema: executableSchema,
context: {
opticsContext: OpticsAgent.context(req),
},
}), {
configServer: (graphQLServer) => {
graphQLServer.use('/graphql', OpticsAgent.middleware());
},
});

Blaze

If you are looking to integrate Apollo with Blaze, you can use the swydo:blaze-apollo package:

1
2
3
4
5
import { setup } from 'meteor/swydo:blaze-apollo';
const client = new ApolloClient(meteorClientConfig());
setup({ client });

This gives you reactive GraphQL queries in your templates!

1
2
3
4
5
6
7
Template.hello.helpers({
hello() {
return Template.instance().gqlQuery({
query: HELLO_QUERY
}).get();
}
});
Edit on GitHub