Apollo-server-express Server Setup for Subscriptions?

I’m updating my code to use apollo-server-express. My queries and mutations are working, but I don’t quite have subscriptions working yet.

Does someone here have server setup code they could post?

Here’s my current setup code:

Server

import { ApolloServer } from 'apollo-server-express';
import { createServer } from 'http';
import {WebApp} from 'meteor/webapp';
import {getUser} from 'meteor/apollo';
import express from 'express';
import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import typeDefs from '/imports/apollo/schema';
import {getUserData, resolvers} from "./api/resolvers";

// https://www.apollographql.com/docs/apollo-server/data/subscriptions/#enabling-subscriptions
// Create the schema, which will be used separately by ApolloServer and
// the WebSocket server.
const schema = makeExecutableSchema({typeDefs, resolvers});

// Create an Express app and HTTP server; we will attach both the WebSocket
// server and the ApolloServer to this HTTP server.
const app = express();
const httpServer = createServer(app);

// Create our WebSocket server using the HTTP server we just set up.
const wsServer = new WebSocketServer({
    server: httpServer,
    path: '/graphql',
});

// Save the returned server's info so we can shutdown this server later
const serverCleanup = useServer({schema}, wsServer);

// Set up ApolloServer.
const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: async ({req}) => ({
        user: await getUser(req.headers.authorization)
    }),
    plugins: [
        // Proper shutdown for the HTTP server.
        ApolloServerPluginDrainHttpServer({httpServer}),

        // Proper shutdown for the WebSocket server.
        {
            async serverWillStart() {
                return {
                    async drainServer() {
                        await serverCleanup.dispose();
                    },
                };
            },
        },
    ],
});

export async function startApolloServer() {
    await server.start();
    const app = WebApp.connectHandlers;

    server.applyMiddleware({
        app,
        cors: true
    });

    httpServer.listen(PORT, () => {
        console.log(
            `Server is now running on ${baseUrl}:${PORT}${server.graphqlPath}`,
        );
    })
}

Client

import React from 'react';
import { InMemoryCache, ApolloProvider, ApolloClient, ApolloLink } from '@apollo/client';
import { BatchHttpLink } from '@apollo/client/link/batch-http'
import { MeteorAccountsLink } from 'meteor/apollo'

// https://www.apollographql.com/docs/react/data/subscriptions/
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';

const wsLink = new GraphQLWsLink(createClient({
    url: 'ws://localhost:4000/subscriptions',
}));

const cache = new InMemoryCache();

const link = ApolloLink.from([
    MeteorAccountsLink(),
    new BatchHttpLink({
        uri: '/graphql'
    })
]);

const client = new ApolloClient({
    uri: '/graphql',
    cache,
    link,
});

export default client;

My setup:

import { ApolloServer } from "apollo-server-express";
import { WebApp } from "meteor/webapp";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/lib/use/ws";

// create and use the websocket server to listen subscriptions
const wsServer = new WebSocketServer({
  noServer: true,
  path: "/graphql",
});
WebApp.httpServer.on("upgrade", function upgrade(request, socket, head) {
  if (!request.url) {
    return;
  }
  switch (request.url) {
    case "/graphql":
      return wsServer.handleUpgrade(request, socket, head, function done(ws) {
        wsServer.emit("connection", ws, request);
      });
    default:
      break;
  }
});

useServer(
  {
    context: async (ctx, msg, args) => {
      const req = ctx.extra.request;

      const loginToken = ctx.connectionParams?.authorization;
      const userAgent = req.headers["user-agent"];
      const clientAddress = getIpAddressFromRequestHeader(req.headers);
      const user =
        typeof loginToken === "string" ? getUserByToken(loginToken) : null;
      const device =
        typeof loginToken === "string"
          ? getDeviceByLoginToken(user?.devices, loginToken)
          : undefined;

      return {
        loginToken,
        user: user || null,
        device,
        userAgent,
        clientAddress,
      };
    },
    schema,
  },
  wsServer
);

Thanks! Could you please include the code for ApolloServer as well? My first take on integrating your code with my ApolloServer code that handles queries/mutations isn’t quite working yet. :grinning:

My apollo server:

const apolloServer = new ApolloServer({
  schema,
  context: async ({ req }) => {
    const loginToken = req.headers.authorization;
    const userAgent = req.headers["user-agent"];
    const clientAddress = getIpAddressFromRequestHeader(req.headers);
    const user = getUserByToken(loginToken);
    const device = getDeviceByLoginToken(user?.devices, loginToken);
    return {
      loginToken,
      user: user || null,
      device,
      userAgent,
      clientAddress,
    };
  },
});

apolloServer.start().then(() => {
  apolloServer.applyMiddleware({
    app: WebApp.connectHandlers,
    cors: true,
    path: "/graphql",
  });
});

May I ask, is this what you have on your client?

import React from 'react';
import { InMemoryCache, ApolloProvider, ApolloClient, ApolloLink } from '@apollo/client';
import { BatchHttpLink } from '@apollo/client/link/batch-http'
import { MeteorAccountsLink } from 'meteor/apollo'

const cache = new InMemoryCache();

const link = ApolloLink.from([
    MeteorAccountsLink(),
    new BatchHttpLink({
        uri: '/graphql'
    })
]);

const client = new ApolloClient({
    uri: '/graphql',
    cache,
    link,
});

export default client;

Yes, very close, something like this:

import {
  InMemoryCache,
  ApolloProvider,
  ApolloClient,
  ApolloLink,
} from "@apollo/client";
import { BatchHttpLink } from "@apollo/client/link/batch-http";
import { onError } from "@apollo/client/link/error";

const cache = new InMemoryCache().restore(window.__APOLLO_STATE__);

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors)
    graphQLErrors.forEach(({ message, locations, path }) =>
      console.log(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
      )
    );
  if (networkError) console.log(`[Network error]: ${networkError}`);
});

const link = ApolloLink.from([
  errorLink,
  // MeteorAccountsLink(),
  new BatchHttpLink({
    uri: "/graphql",
  }),
]);

const client = new ApolloClient({
  cache,
  link,
});

Actually I don’t (not yet) use Apollo on web client. I use it on React Native app.

Oh okay, maybe there’s a minor but hard-to-find difference in React Native vs. web client in this regard. I couldn’t get it working with web client yet, but I solved my blocker another way. I’m continuing with the excellent swydo:ddp-apollo, which makes the apollo-meteor connections easy to set up.

1 Like

So [swydo:ddp-apollo ] still works with Meteor 2.7?
Should one scaffold an ddp-apollo based app with:

  • meteor create --apollo or with
  • meteor create --react?

Yes, it continues to work very well.

It appears to me that Apollo is more work to set up than React, so I would use meteor create --apollo and let Meteor set it all up for you.