next.js
next.js copied to clipboard
Update most Apollo Server examples to Apollo Server 3
Previously, most of the examples used Apollo Server 2. As a maintainer of Apollo
Server, we frequently found that users would start with one of these examples,
try to upgrade to Apollo Server 3, and get confused when it didn't work. Apollo
Server 3 has a more explicit lifecycle that requires awaiting a start method
on startup, and users often put this in the wrong place. The
api-routes-graphql package did use Apollo Server 3 and did call start, but
only awaits it when the first request comes in rather than at the top level.
Additionally, all the examples use apollo-server-micro. While this package is
technically a maintained part of the Apollo Server project, it is not as fully
featured or battle-tested as the most popular package,
apollo-server-express. For example, it doesn't have the same default CORS
behavior has the other Apollo Server framework integrations, and doesn't have a
way to drain servers gracefully on shutdown. (Future Apollo Server features may
target apollo-server-express before other integrations as well.) Because
Next.js can easily use Express middleware with just a few lines of integration
code (documented at
https://nextjs.org/docs/api-routes/api-middlewares#connectexpress-middleware-support),
Next.js users would be best served by using the most mainstream ApolloServer
implementation rather than the least maintained one.
So this PR:
- Changes all examples from using
apollo-server-microv2 toapollo-server-expressv3 - Uses top level await (enabled in next.config.js) for proper startup handling
- Upgrades other packages like
graphqland@graphql-tools/schema, and installs them where relevant - Removes special CORS handling from
examples/api-routes-graphqlbecauseapollo-server-expresshas better CORS defaults. (If the default is not good enough for this example, pass acorsoption togetMiddlewareinstead of doing CORS manually. The value of thecorsoption is equivalent to the argument to the npmcorspackage's middleware.)
This leaves the with-apollo-neo4j-graphql example alone, as I could not get it
to work properly before or after upgrading Apollo Server.
Documentation / Examples
- [x] Make sure the linting passes
@elrumordelaluz I'm not a Next.js expert but I don't think that's true here. We're not actually using Express --- we're just converting an Express-style middleware into a Next.js-style handler using the pattern from the Next.js docs.
We're not actually using Express --- we're just converting an Express-style middleware into a Next.js-style handler using the pattern from the Next.js docs.
Did you tried to run one of those examples? I replicate your changes in one of my projects setup and this warning throws:
Error: Cannot find module 'express'
Require stack:
- /…/node_modules/apollo-server-express/dist/ApolloServer.js
...
Seems to be due to this.
@elrumordelaluz Hmm, I did run them and they worked for me, but you're right that there is a runtime dependency on express (for express.Router(). Let me see why they worked for me and add the dependencies.
@elrumordelaluz I'm not sure why it worked for me before; maybe a difference in what npm/yarn installation we used which made mine install peer dependencies automatically? Anyway, added the dependency to all the examples.
I have been using Apollo Server with Next.js from some time and was about to start a new project. I checked out this example and to be honest, this feels like change for the worse, compared to previous starter. The fact that is uses webpack experimental topLevelAwait, it requires express dependency, and it relies on a function that pretends to be express middleware, makes it feel really fragile. I wouldn't use it personally.
@patryk-smc could you expand a little more?
I am also using Apollo in several NextJs projects, and I feel more that the change is between Apollo 2 and 3. In case you were using Apollo 3, what is the main difference between using apollo-server-micro + micro deps and apollo-server-express and express?
The issue with the experimental could be bypassed with something like:
const apolloServer = new ApolloServer({ /* ... */ })
const startServer = apolloServer.start()
export default async function handler(req, res) {
// ...
await startServer
await apolloServer.createHandler({
path: '/api/graphql',
})(req, res)
}
Sincerely don't know about heavyness of each apollo-server-* package and which one is more maintained than the other (all stuff discussed in the issues related), but I think the most important point is that the community mostly copy/paste examples assuming are best-practices, but not always this is true (this is also discussed there).
Hope in one of this threads/discussions we can arrive into a solution for the best composition using Apollo 3 and Next.
I have been using Apollo Server with Next.js from some time and was about to start a new project. I checked out this example and to be honest, this feels like change for the worse, compared to previous starter. The fact that is uses webpack experimental
topLevelAwait, it requires express dependency, and it relies on a function that pretends to be express middleware, makes it feel really fragile. I wouldn't use it personally.
I agree that this new version requiring topLevelAwait and express is kind of a pain and I much prefer the simplicity of using apollo-server-micro but apparently that package isn't really supposed to be used in production, and the only people really using it are those basing it off the Next.js examples.
Even if you want to stick with apollo-server-micro you'll still need topLevelAwait if you want to upgrade to v3.
The problem with the await startServer solution (assuming I understand how Next.js works properly) is that this means "if there's a startup error, then make every handler invocation fail", whereas it makes more sense to be "don't even start listening for incoming connections until the server has properly started up".
The problem with using apollo-server-micro is that it was an externally-contributed package with literally 3% of the usage of apollo-server-express that frankly we shouldn't have accepted into the project. (And as far as I can tell the only reason that this 3% number is so high is because of these Next.js examples.) It already does not support all of Apollo Server's features and we are likely to not target it for upcoming features. There is a very good chance that a future version of AS will not include it (our hope is to make the interface between Apollo Server and web frameworks into a more stable API and turn most of the integrations into community-supported rather than core-supported).
Given that (as far as I understand) none of the examples actually use micro itself at all, it doesn't do Next.js users a favor to tie them to apollo-server-micro. That package should really only be used in conjunction with micro.
FWIW, if part of the issue is that having to stick runMiddleware in every example is annoying, I'd be happy to make that easier, by doing something like publishing a tiny npm package that just exports runMiddleware, or even making apollo-server-express have a method that returns a more micro-style middleware. But it doesn't seem like runMiddleware is the sticking point here?
Thanks for the micro background, now it all makes sense.
For me, the biggest annoyance is that it requires an experimental flag, meaning that every minor webpack update could potentially break the thing. Chances are low, but still, it's a good practice to not relay on experimental stuff in production.
The middleware is not a big deal, but it would be nice to have it as a part of apollo-server-express. It would for sure make it easier for folks to get started and not have them to think why it has to be done that way.
For me, the biggest annoyance is that it requires an experimental flag, meaning that every minor webpack update could potentially break the thing. Chances are low, but still, it's a good practice to not relay on experimental stuff in production.
Sure. But on the other hand, the idea that a server might have to do async work on startup and shouldn't start serving incoming traffic until that work succeeds seems like a pretty basic capability for a server. So if webpack were to remove this later, hopefully Next.js could find another mechanism for achieving the same goal? Or if that really isn't possible, then at that point the examples could be changed to the worse "server starts up but every request fails" semantics. (This change in Apollo Server wasn't undertaken lightly — "my server had a plugin whose startup hook failed but my server runs anyway" was a major problem in Apollo Server 2 that many users ran into.)
The middleware is not a big deal, but it would be nice to have it as a part of apollo-server-express. It would for sure make it easier for folks to get started and not have them to think why it has to be done that way.
FWIW this runMiddleware was taken directly from the Next.js docs. Maybe it would make sense for it to just be exported from next or some sort of new package like @next/express-middleware? Then the Next.js docs could just say "to use Express middleware, use import { runMiddleware } from ....
@glasser As NextJS is often deployed in serverless environments, would it be more suitable for it to use .ensureStarted()? Would this help with peoples concerns around using top-level await?
I mean, I'm not exactly sure what "serverless" means here, in a more concrete sense than "runs on platforms like Lambda". If there's an issue with AS startup (ie, start throws) then that ApolloServer object will never be able to run operations successfully. So the best approach is whatever is most likely to result in the object being re-created, which might be process failure in this case. (But the best approach is not to start a brand new server on each handler invocation, because then you're doing startup more than is necessary.)
apollo-server-lambda does do something like this (https://github.com/apollographql/apollo-server/blob/c1fd1798a79343bf5803c14d82ad2279e9449ccf/packages/apollo-server-lambda/src/ApolloServer.ts#L38-L61) but that's because the Lambda runtime really doesn't support any sort of top-level async work, whereas it appears that Next.js's runtime does support that as long as you turn on the experimental flag.
Yes I noticed that's how the lambda version is implemented. Top level await is based upon on the Node runtime being >14.8, which I believe is supported by AWS lambda.
Since Node 14 is the current LTS, I think top level await should work fine, but maybe it's worth leaving a comment about .ensureStarted() for anyone still running Node 12?
I've tried to make the same changes in my own project as seen here to see how it would work. Unfortunately, I've gotten some errors that seem to have to do with express, but I have no idea what they mean.
warn - ./node_modules/express/lib/view.js
Critical dependency: the request of a dependency is an expression
Unhandled Runtime Error
TypeError: Cannot read properties of undefined (reading 'prototype')
Call Stack
eval
node_modules/express/lib/response.js (42:0)
Object../node_modules/express/lib/response.js
file:///.../.next/static/chunks/pages/_app.js (3827:1)
@tettoffensive The first one looks like a webpack error. I'm not very familiar with webpack. Are you able to share your configuration in a way I could reproduce it? Do these issues appear on the actual examples from this PR for you?
Is it possible for a maintainer to approve running workflows on this so I can see if it is passing CI?
FYI: I successfully transitioned to apollo-server-express based on this example, but after I wanted to check new middleware functionality in [email protected], I noticed that webpack topLevelAwait: true breaks the whole next.js app.
@patryk-smc interesting, what do you mean by "breaks the whole next.js app"? Like, it just gives an error saying topLevelAwait is not supported? Or using it doesn't work well?
@glasser the next.js app just hangs, there is no error message, nothing, in terminal I see this:

And when going to http://localhost:3000/ it is indefinitely loading.
@patryk-smc Are you able to reproduce that perhaps with one of these examples by forking off this branch?
@glasser: This is my next.config.js:
webpack: (config, { isServer }) => {
config.experiments = {
topLevelAwait: true,
};
if (!isServer) {
config.resolve.fallback.fs = false;
}
return config;
},
I changed to match yours and got a different error which led me to remove importing the ApolloError class in a file responsible for client-side requests. Once I did that and just made a new type (I'm using TypeScript) called ApolloError. It seemed to work. I was even able to restore my original webpack config and still works fine.
I'm not really sure why that happened. I'm able to import ApolloError else where in my client with no issue so far.
@glasser
I created a new app via yarn create next-app --typescript, added apollo-server-express, made sure that it works, and then updated to next canary.
Link: https://github.com/patryk-smc/next-apollo-repro
Steps:
yarn installyarn run dev- Go to http://localhost:3000/api/graphql
- You should see errors in terminal and the browser should hang
@patryk-smc You need to update your next.config.js to
config.experiments = {
...config.experiments,
topLevelAwait: true,
};
I believe the canary branch enables experimental Webpack features. By overriding the entire experiments object, you are disabling required flags.
@marklawlor I'll update the examples to use that syntax in a bit.
It would be great if somebody from the project could enable workflows so I can see if I'm passing CI or not.
I've updated to add ...config.experiments.
Seems to be broken, upgrading into [email protected].
error - ./pages/api/graphql.js
Error: error: top level await requires target to es2017 or higher and topLevelAwait:true for ecmascript
|
53 | await apolloServer.start()
| ^^^^^
Caused by:
0: failed to process js file
1: error was recoverable, but proceeding would result in wrong codegen
2: Syntax Error
From the micro repo:
Disclaimer: Micro was created for use within containers and is not intended for use in serverless environments. For those using Vercel, this means that there is no requirement to use Micro in your projects as the benefits it provides are not applicable to the platform.
I guess the same holds true for Express based Apollo server.
We need an proper Next.js ApolloServer. Serverless (whatever that means for ApolloServer but you can set serverlessFramework to return true), using ensureStarted like mentioned.
You can find the code for a POC in this gist. It supports a landing page and file uploads (optionally) but not much more than that. Also note that path is not needed. Using ApolloServer in Next.js should be as simple as:
// pages/api/graphql.js
const apolloServer = new ApolloServer({ ... })
export default apolloServer.createHandler()
@CrocoDillon I'll admit: I don't know much about Next.js. My motivation for this PR is primarily to stem the flood of folks opening issues on the AS repo that are based on them starting with these examples and having trouble upgrading to AS3.
So I don't even really know what the intended deployment strategy for Next.js (and specifically, the server API routes feature) is. My understanding is that it is designed to be run either in a "traditional" 'a server starts up and runs and serves stuff with parallelism' fashion or in a "serverless" 'a server starts up quickly, runs only one request at a time, and can be killed between requests' fashion.
Apollo Server is a stateful project that does work on startup that can fail. So if you are running in a "traditional" setup, it really does seem to me that (a) that work should be done when the server starts, not on the first request, and that (b) if the startup code fails, the whole server should permanently fail rather than starting in a mode where requests serve 5xx errors to clients. So I'm a bit loathe to change these examples to be entirely tailored to the serverless use case (where "doing work on startup" is harder) unless I'm confused and Next.js really only targets "serverless".
Maybe one solution here would be to have an example that shows a "traditional server with startup error handling" use case (using top-level await), and a second example that shows a "serverless deployment" use case? (Maybe we could delete a bunch of the pretty similar examples while we're at it?)
@glasser I believe that api routes are meant to be serverless. Next.js (used to) supports a custom traditional server but that's either discouraged or deprecated (not sure which) as it disables a lot of the benefits from Next.js, for example SSG and easy deployments to Vercel.
So there's no recommended path for doing initialization on startup in a Next.js app? Only when a request comes in? I suppose I can try to change these examples to use ensureStarted; it just seems like a shame that anyone using Next.js in a more stateful server would end up with worse error handling.
My honest goal here is to stop getting bug reports on my project from folks who try to upgrade these examples and do it incorrectly, so I'm down to do whatever changes are necessary to get this merged. I can't tell if anyone who's responded yet is a project owner — I see that nobody has enabled CI for this PR yet.
@glasser I've been following in the background. I don't really know enough about this to comment on your work. But it seems like you have a good goal, so I appreciate the work you've put in. And will likely use whatever solution is reached in this PR. FWIW I use apollo-server-micro with apollo 3 on a server, not Vercel.
Anyways it may help to tag @ijjk ( Code owner of 20 files in this pull request ). Hope you folks can reach an agreement that is good for everyone. Good luck and thank you :)