Concept
Version-currency callout, the anchor fact for this entire domain: the apollo-server package, the one new ApolloServer({ typeDefs, resolvers }).listen() pattern a huge amount of existing tutorials, courses, and Stack Overflow answers still teach, is confirmed genuinely gone. Attempting to resolve it in this app's dependency tree throws MODULE_NOT_FOUND. This isn't a deprecation warning still working in the background; the package that shape of code depends on simply isn't part of the current Apollo Server story.
The current, confirmed-working setup: @apollo/server + an explicit transport
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
const typeDefs = `#graphql
type Query { hello: String }
`;
const resolvers = { Query: { hello: () => "world" } };
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`Server ready at ${url}`);Confirmed by running this exact code: a real server starts, and a real HTTP POST with a GraphQL query ({ query: "{ hello }" }) returns a real, correct result ({"data":{"hello":"world"}}). The current major version genuinely restructured the package: ApolloServer itself no longer has a .listen() method, starting the server requires an explicit transport function, of which startStandaloneServer (for a simple, self-contained server) is one option.
Production integration: Express, via a dedicated integration package
import express from "express";
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@as-integrations/express5";
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
const app = express();
app.use(
"/graphql",
express.json(),
expressMiddleware(server, {
context: async ({ req }) => ({ requestId: crypto.randomUUID() }),
})
);
app.listenConfirmed by running this exact setup end-to-end (real Express app, real HTTP request, real resolved result): for a real production app that needs Apollo Server mounted alongside other Express routes (rather than standing entirely alone), @as-integrations/express5's expressMiddleware is the current, correct integration path, a separate, dedicated package rather than something bundled into @apollo/server itself.
The context function: confirmed fresh per request
context: async ({ req }) => ({
requestId: crypto.randomUUID(),
postsLoader: new DataLoader(batchFetchPosts), // a FRESH loader, every request
}),Confirmed by running this exact pattern with a request-scoped random value: the context function is called fresh for every incoming request, and whatever it returns becomes available to every resolver handling that request. This is precisely the mechanism that makes the DataLoader-per-request pattern from N+1 Problem & DataLoader actually work, instantiating a DataLoader inside this function, rather than once at server startup, is what guarantees each request gets its own fresh, correctly-scoped loader instance.
Try It
Predict the outcome before checking the solution.
const server = new ApolloServer({ typeDefs, resolvers });
server.listen({ port: 4000 }); // written by someone following an old tutorialDoes this code work against the currently installed @apollo/server?
Solution
No, ApolloServer instances in the current major version don't have a .listen() method at all; that was part of the older apollo-server package's API, confirmed to no longer exist in this project's dependency tree. This code would fail with something like "server.listen is not a function." The correct current approach is calling an explicit transport function, startStandaloneServer(server, { listen: { port: 4000 } }) for a standalone server, or mounting expressMiddleware(server, ...) into an existing Express app and calling app.listen() on the Express app itself, not on the Apollo server instance.
Implement It Yourself
Build a minimal request-scoped context factory, to internalize why the context function runs per-request rather than once:
function createContextFactory(sharedConfig) {
return async ({ req }) => {
// this function runs ONCE PER INCOMING REQUEST, everything created
// here is scoped to THIS request only, never shared across requests
return {
requestId: crypto.randomUUID(),
userLoader: new DataLoader(sharedConfig.batchFetchUsers),
postsLoader: new DataLoader(sharedConfig.batchFetchPosts),
};
};
}
// usage:
expressMiddleware(server, { context: createContextFactory({ batchFetchUsers, batchFetchPosts }) });Every resolver handling a given request receives the SAME context object (so loaders created here are correctly shared within that one request, enabling batching), but a completely different, freshly-created context object for every other request, which is exactly the isolation the DataLoader-per-request rule depends on.
Under the Hood
Apollo Server's standalone mode runs directly on top of Node's own http module, the request/response lifecycle, keep-alive behavior, and streaming mechanics covered in HTTP Server Fundamentals are the actual foundation underneath, not something Apollo Server replaces. The context function's per-request scoping is what makes the DataLoader-per-request pattern from N+1 Problem & DataLoader concretely implementable, this topic supplies the where (the context function) for that pattern's what.
Common Mistakes
1. Following tutorials that use the apollo-server package or .listen() directly
import { ApolloServer } from "apollo-server"; // ❌ confirmed: this package doesn't resolveConfirmed via direct testing, code depending on this package fails immediately at the import step, not with a subtle runtime issue.
2. Creating a DataLoader outside the context function
const sharedLoader = new DataLoader(batchFetchPosts); // ❌ created ONCE, at module load
const server = new ApolloServer({ typeDefs, resolvers, context: () => ({ postsLoader: sharedLoader }) });This defeats the entire per-request isolation the context function provides, confirmed in N+1 Problem & DataLoader to cause real cross-request data leakage via DataLoader's persistent cache.
3. Forgetting await server.start() before mounting Express middleware
const server = new ApolloServer({ typeDefs, resolvers });
app.use("/graphql", expressMiddleware(server, { context })); // ❌ missing server.start() firstApollo Server needs to complete its own async startup before being wired into Express, this is an easy step to miss coming from the standalone-server pattern, where startStandaloneServer handles both starting and serving in one call.
Best Practices
- Use
startStandaloneServerfor a simple, self-contained GraphQL server, and a dedicated framework integration package (like@as-integrations/express5) when Apollo Server needs to coexist with other routes in an existing Express (or similar) app. - Always instantiate request-scoped resources, DataLoaders chief among them, inside the
contextfunction, never at module scope. - Verify any Apollo Server tutorial or code snippet against the actually-installed major version before trusting it, this ecosystem specifically has had more breaking API churn across versions than most libraries covered in this curriculum.
- Remember
await server.start()is required before mounting framework middleware, even thoughstartStandaloneServerhandles this internally for the standalone case.
Performance Tips
- The
contextfunction running per-request has a real, if typically small, per-request cost, keep it focused on genuinely request-scoped setup (loaders, auth context) rather than expensive work that could be done once at server startup instead. - Since Apollo Server's standalone/Express integrations run on Node's own HTTP server underneath, the same keep-alive connection-reuse benefits covered in HTTP Server Fundamentals apply directly here too, no separate GraphQL-specific connection handling to reason about.
