Skip to content

Conversation

gmenih
Copy link

@gmenih gmenih commented Jul 28, 2025

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

Issue Number: N/A

What is the new behavior?

@apollo/server@v5 is now supported, along with as express@v5

Does this PR introduce a breaking change?

  • Yes
  • No

An additional dependency is introduced - @as-integrations/express5; not an API breaking change, but projects using this package will have to install the new dependency. I'm happy to update the docs if this gets approved.

Other information

Technically both express@v4 and express@v5 are supported by @apollo/server@v5, but they require separate dependencies - either @as-integrations/express4 or @as-integrations/express5.

Considering that @nestjs/platform-express is on v5, I used v5 here as well.

@gmenih gmenih mentioned this pull request Jul 28, 2025
1 task
@gmenih
Copy link
Author

gmenih commented Aug 1, 2025

@kamilmysliwiec is there anything I can do to help move this forward? I'd love to start using this in our project 🙌

@jet-snowman
Copy link

any updates ?

@hadarziv-army
Copy link

I’d like to start using this in a project as well. Are there any updates?

@vishalsabhaya
Copy link

It will support fastify as well?

@hadeed85
Copy link

any updates on this?

@gmenih
Copy link
Author

gmenih commented Aug 18, 2025

@kamilmysliwiec awaiting your response - any comments on this PR?

@OpCoreEngine
Copy link

@luas10c
Copy link
Contributor

luas10c commented Aug 29, 2025

It makes a lot of sense to stop using @as-integrations/express5 and create middleware within the package.

middleware.ts

import type { WithRequired } from '@apollo/utils.withrequired'
import type express from 'express'
import type {
  ApolloServer,
  BaseContext,
  ContextFunction,
  HTTPGraphQLRequest
} from '@apollo/server'
import { HeaderMap } from '@apollo/server'

export interface ExpressContextFunctionArgument {
  request: express.Request
  response: express.Response
}

export interface ExpressMiddlewareOptions<TContext extends BaseContext> {
  context?: ContextFunction<[ExpressContextFunctionArgument], TContext>
}

export function expressMiddleware(
  server: ApolloServer<BaseContext>,
  options?: ExpressMiddlewareOptions<BaseContext>
): express.RequestHandler
export function expressMiddleware<TContext extends BaseContext>(
  server: ApolloServer<TContext>,
  options: WithRequired<ExpressMiddlewareOptions<TContext>, 'context'>
): express.RequestHandler
export function expressMiddleware<TContext extends BaseContext>(
  server: ApolloServer<TContext>,
  options?: ExpressMiddlewareOptions<TContext>
): express.RequestHandler {
  server.assertStarted('expressMiddleware()')

  // This `any` is safe because the overload above shows that context can
  // only be left out if you're using BaseContext as your context, and {} is a
  // valid BaseContext.
  const defaultContext: ContextFunction<
    [ExpressContextFunctionArgument],
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    any
  > = async () => ({})

  const context: ContextFunction<[ExpressContextFunctionArgument], TContext> =
    options?.context ?? defaultContext

  return async (request, response) => {
    if (!('body' in request)) {
      // The json body-parser *always* initializes the `body` field on requests
      // when it runs.  (body-parser@1 (included in Express v4 as
      // `express.json()`) sets it to `{}` by default, and body-parser@2
      // (included in Express v5 as `express.json()`) sets to to `undefined` by
      // default.)
      //
      // So if the field is *completely* missing (not merely set to undefined,
      // but actually not there), you probably forgot to set up body-parser. We
      // send a nice error in this case to help with debugging.

      return void response
        .status(500)
        .send(
          '`req.body` is not set; this probably means you forgot to set up the ' +
            '`json` middleware before the Apollo Server middleware.'
        )
    }

    const headers = new HeaderMap()
    for (const [key, value] of Object.entries(request.headers)) {
      if (value !== undefined) {
        // Node/Express headers can be an array or a single value. We join
        // multi-valued headers with `, ` just like the Fetch API's `Headers`
        // does. We assume that keys are already lower-cased (as per the Node
        // docs on IncomingMessage.headers) and so we don't bother to lower-case
        // them or combine across multiple keys that would lower-case to the
        // same value.
        headers.set(key, Array.isArray(value) ? value.join(', ') : value)
      }
    }

    const httpGraphQLRequest: HTTPGraphQLRequest = {
      method: request.method.toUpperCase(),
      headers,
      search: new URL(request.url).search ?? '',
      body: request.body
    }

    const httpGraphQLResponse = await server.executeHTTPGraphQLRequest({
      httpGraphQLRequest,
      context: () => context({ req: request, res: response })
    })

    for (const [key, value] of httpGraphQLResponse.headers) {
      response.setHeader(key, value)
    }
    response.statusCode = httpGraphQLResponse.status || 200

    if (httpGraphQLResponse.body.kind === 'complete') {
      return void response.send(httpGraphQLResponse.body.string)
    }

    for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
      response.write(chunk)
      // Express/Node doesn't define a way of saying "it's time to send this
      // data over the wire"... but the popular `compression` middleware
      // (which implements `accept-encoding: gzip` and friends) does, by
      // monkey-patching a `flush` method onto the response. So we call it
      // if it's there.
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      if (typeof (response as any).flush === 'function') {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        ;(response as any).flush()
      }
    }
    return void response.end()
  }
}

@luas10c
Copy link
Contributor

luas10c commented Aug 29, 2025

Alternative above removes unnecessary dependencies like for url parsing, now uses new URL() constructor

@Kian738
Copy link

Kian738 commented Aug 31, 2025

Is this production ready yet?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants