I've been trying to implement a websocket server with Apollo Server in Next.js for 5 days now and still can't get anything to work. I tried a lot of different implementations and the one i was close to get the job works was with apollo-server-micro package. Close but still not working... I drop you bellow my config of Apollo Server and Apollo Client, and the resolver i'm using for the subscription.
src/pages/api/graphql.ts
import { ApolloServer } from 'apollo-server-micro';
import { useServer } from 'graphql-ws/lib/use/ws';
import { WebSocketServer } from 'ws';
import resolvers from '@/graphql/resolvers/resolvers';
import typeDefs from '@/graphql/schemas/typeDefs';
import { prisma } from '@/server/prismaClient';
import { makeExecutableSchema } from '@graphql-tools/schema';
const schema = makeExecutableSchema({ typeDefs, resolvers });
export const config = {
api: {
bodyParser: false,
externalResolver: true,
},
};
// Apollo Server --------------------------------------------------------------
const apolloServer = new ApolloServer({
schema,
cache: 'bounded',
context({ req }) {
const { socket } = req;
return { pubsub: socket?.server?.pubsub, prisma };
},
plugins: [
{
async serverWillStart() {
return {
async drainServer() {
console.log(`ApolloServer: drain server :>>>>>>>>>>>>>>>>>>`);
if (graphqlWSS) {
console.log(`ApolloServer: disposing WSS :>>>>>>>>>>>>>>>>>>`);
await graphqlWSS.dispose();
}
},
};
},
},
],
});
const startServer = apolloServer.start();
let graphqlWSS;
let apolloServerHandler;
const handler = async (req: any, res: any) => {
if (req.method === 'OPTIONS') {
return res.end();
}
const oldApolloServer = res.socket.server.apolloServer;
if (oldApolloServer && oldApolloServer !== apolloServer) {
console.warn('Fixing Apollo Server hot reload');
oldApolloServer.stop();
delete res.socket.server.apolloServer;
}
if (!res.socket.server.apolloServer) {
res.socket.server.apolloServer = apolloServer;
if (!graphqlWSS) {
/* eslint-disable react-hooks/rules-of-hooks */
console.log(`Initializing GraphQL WSS :>>>>>>>>>>>>>>>>>>`);
const wss = new WebSocketServer({
server: res.socket.server,
path: '/api/graphql',
});
graphqlWSS = useServer(
{
schema,
context() {
const { socket } = req;
return { pubsub: socket.server.pubsub };
},
},
wss
);
}
await startServer;
apolloServerHandler = apolloServer.createHandler({ path: '/api/graphql' });
}
await apolloServerHandler(req, res);
};
export default handler;
src/pages/_app.tsx
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import { CssBaseline } from '@mui/material';
import { ThemeProvider } from '@mui/material/styles';
import { SessionProvider } from 'next-auth/react';
import Router from 'next/router';
import NProgress from 'nprogress';
import { Provider } from 'react-redux';
import { ToastContainer } from 'react-toastify';
import { HttpLink, split } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';
import store from '../redux/store';
import { hmtTheme } from '../styles/theme';
// Styles
import 'nprogress/nprogress.css';
import 'react-toastify/dist/ReactToastify.css';
import 'tippy.js/dist/tippy.css';
import '../styles/globals.css';
const httpLink = new HttpLink({
uri: '/api/graphql',
});
const wsLink =
typeof window !== 'undefined'
? new GraphQLWsLink(
createClient({
url: 'ws://localhost:3000/api/graphql',
})
)
: null;
const link =
typeof window !== 'undefined' && wsLink != null
? split(
({ query }) => {
const def = getMainDefinition(query);
return (
def.kind === 'OperationDefinition' &&
def.operation === 'subscription'
);
},
wsLink,
httpLink
)
: httpLink;
const client = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
ChatConversation: {
fields: {
messages: {
merge(
existing: any[],
incoming: any[],
{ readField, mergeObjects }
) {
const merged: any[] = existing ? existing.slice(0) : [];
const messageToIndex: Record<string, number> =
Object.create(null);
if (existing) {
existing.forEach((message, index) => {
messageToIndex[readField<string>('id', message)] = index;
});
}
incoming.forEach((message) => {
const id = readField<string>('id', message);
const index = messageToIndex[id];
if (typeof index === 'number') {
// Merge the new author data with the existing author data.
merged[index] = mergeObjects(merged[index], message);
} else {
// First time we've seen this author in this array.
messageToIndex[id] = merged.length;
merged.push(message);
}
});
return merged;
},
},
},
},
},
}),
link,
uri: '/api/graphql',
});
NProgress.configure({ showSpinner: false });
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
function App({ Component, pageProps: { session, ...pageProps } }) {
return (
<ApolloProvider client={client}>
<SessionProvider session={session}>
<Provider store={store}>
<ThemeProvider theme={hmtTheme}>
<ToastContainer
containerId="app"
draggable={false}
position="bottom-right"
toastStyle={{ backgroundColor: '#252525', color: '#ecf0f1' }}
/>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
</Provider>
</SessionProvider>
</ApolloProvider>
);
}
export default App;
src/server/graphql/resolvers/chatMessageResolver.ts
import { pubsub } from '@/server/pubSubInstanciate';
import { CreateChatMessageInput } from '../types';
import { ChatMessageRepository } from './../../repositories/chatMessageRepository';
const chatMessageRep = new ChatMessageRepository();
const chatMessageResolver = {
Query: {
chatMessages: async (
_: undefined,
_args: undefined,
_parent: undefined
) => {
return await chatMessageRep.findAll();
},
chatMessagesByUserId: async (
_: undefined,
{ userId }: { userId: string },
_parent: undefined
) => {
return await chatMessageRep.findByUserId(userId);
},
},
Mutation: {
createChatMessage: async (
_: undefined,
{
userId,
createChatMessageInput,
}: { userId: string; createChatMessageInput: CreateChatMessageInput },
ctx: any
) => {
const chatMessage = await chatMessageRep.create(
userId,
createChatMessageInput
);
pubsub.publish('CHAT_MESSAGE_CREATED', {
chatMessageCreated: chatMessage,
});
return chatMessage;
},
},
Subscription: {
chatMessageCreated: {
subscribe: (_: undefined, _args: undefined, ctx: any) =>
pubsub.asyncIterator(['CHAT_MESSAGE_CREATED']),
resolve: (payload) => {
return payload && payload !== null;
},
},
},
};
export default chatMessageResolver;
I also tried this following command to get some informations about my endpoint and this is what it returns
npx diagnose-endpoint@1.1.0 --endpoint=http://localhost:3000/api/graphql
Diagnosing http://localhost:3000/api/graphql
⚠️ OPTIONS response is missing header 'access-control-allow-methods: POST'
⚠️ POST response missing 'access-control-allow-origin' header.
If using cookie-based authentication, the following headers are required from your endpoint:
access-control-allow-origin: https://studio.apollographql.com
access-control-allow-credentials: true
Otherwise, a wildcard value would work:
access-control-allow-origin: *
( Interested in previewing a local tunnel to bypass CORS requirements? Please let us know at https://docs.google.com/forms/d/e/1FAIpQLScUCi3PdMerraiy6GpD-QiC_9KEKVHr4oDL5Vef5fIvzqqQWg/viewform )
Thank you a lot in advance to people can help me. I'm really breaking my head right now...