@occupop/lib-graphql
v5.0.0
Published
This package is intended to be used with `@occupop/lib-container` being a wrapper for Apollo Server, providing factory for server and some helpers.
Maintainers
Keywords
Readme
Package @occupop/lib-graphql
This package is intended to be used with @occupop/lib-container
being a wrapper for Apollo Server, providing factory for server and some helpers.
This implementation refers to https://www.apollographql.com/docs/apollo-server! Check their docs for detailed/advanced usage.
NPM registry token required!
Install
# Yarn
yarn add @occupop/lib-graphql
# NPM
npm install @occupop/lib-graphql
# Bun
bun add @occupop/lib-graphqlgraphqlMiddleware needs express 5 in the consuming project (peer dependency). TypeScript
consumers also need @types/express 5, because the middleware's declarations use express's types:
yarn add express@^5
yarn add -D @types/express@^5A project still on express 4 stays on @occupop/lib-graphql 4.x.
Requirements
- Node.js ≥ 22. Apollo Server 5 and
@as-integrations/express5ask for>=20, but@apollo/subgraph2.15 declares>=22.0.0, and that is the binding floor.yarn1 refuses the install below it (error @apollo/[email protected]: The engine "node" is incompatible with this module);npmonly warns, and the code does run on 20.19 — but 22 is the supported floor. - The package is ESM-only. A CommonJS project loads it through
require(esm), which Node enables by default from 20.19 and 22.12; on older minorsrequire('@occupop/lib-graphql')fails withERR_REQUIRE_ESM. - Jest (CommonJS) cannot load an ESM-only package without a transform. Mock the module in unit tests, or transform it.
- TypeScript 5.x (5.6.3 or later) or 6.x: the
typescriptpeer is^5.6.3 || ^6.0.0since 5.0.0.
5.0.0 — breaking changes
express5 is required.graphqlMiddlewarenow comes from@as-integrations/express5instead of@as-integrations/express4, and theexpresspeer dependency is^5.0.0instead of^4.0.0. The export name and call signature are unchanged. A project on express 4 stays on 4.x: a^4range never resolves 5.0.0.@as-integrations/express4is no longer installed with this package. A project that imported it directly, without declaring it, must declare it itself.- On express 5, a request without a body is served. express 5's JSON parser leaves
req.bodyundefined when a request has no body, as a GET query has none. Under express 5 the express 4 integration answered such a request with HTTP 500 ("req.bodyis not set"); the express 5 integration serves it.express.json()is still mounted in front of the middleware. - Unchanged: Apollo Server 5 (
@apollo/server^5.5.1),@apollo/subgraph^2.15.1,graphql16 (^16.11.0), the exports and their signatures, ESM-only, Node ≥ 22. Thetypescriptpeer now also admits 6.x.
4.0.0 — breaking changes
- Apollo Server 5 (
@apollo/server^5). Apollo Server 4 reached end-of-life on 2026-01-26. Read the upstream migration notes for behaviour that moved — for example, variable-coercion errors now answer HTTP 400 by default. graphqlMiddlewarenow comes from@as-integrations/express4. Apollo Server 5 no longer ships@apollo/server/express4. The export name and call signature are unchanged;express4 must be installed by the consumer.@apollo/subgraph^2.15. TheGraphQLResolverMaptype is imported from the package root. The server is built with the array form (buildSubgraphSchema([{ typeDefs, resolvers }])), which is the only form 2.15 accepts.- Version 4.0.0 follows
3.0.1on npm. The 2.x and 3.x lines exposed a different API (an HTTP server factory and a GraphQL client); this line exposes only the Apollo Server factory and helpers below.
Env Setup
ENV variables suggested
APP_ENV=dev
APP_NAME=nice-service
APP_DEBUG=1
APP_PORT=4000Register
import {
makeGraphqlServer, graphqlMiddleware, graphqlStandalone
} from '@occupop/lib-graphql'
import { resolvers } from './app/resolvers'
//... all other imports omited...
export const container = createTypedContainer({
// ...
graphqlServer: { asFunction: makeGraphqlServer, singleton: true },
graphqlSchema: { asFunction: () => readFileSync(path.resolve(__dirname, 'src/schema.graphql')), singleton: true },
graphqlResolvers: { asValue: resolvers },
graphqlStandalone: { asValue: graphqlStandalone }, // if startStandaloneServer needed
graphqlMiddleware: { asValue: graphqlMiddleware }, // if expressMiddleware needed
// ...
})
About the helpers
The helpers startStandaloneServer and expressMiddleware can be used as graphqlStandalone and graphqlMiddleware (alias is just for naming convenience).
Usage (starting server)
Standalone server
// server.ts
import { container } from '.container'
const { graphqlServer, graphqlStandalone } = container.cradle
const httpPort = process.env.APP_PORT || 4000
await await graphqlStandalone(graphqlServer, {
context: async ({ req }) => ({ userToken: req.headers.token }),
listen: { port: httpPort },
})
console.log(`Server listening on http://localhost:${httpPort}`)Over Express
// server.ts
import express from 'express'
import { container } from '.container'
const { httpServer, graphqlServer, graphqlMiddleware } = container.cradle
await graphqlServer.start() // server require start() call...
const httpPort = process.env.APP_PORT || 4000
await httpServer.use('/graphql', express.json(), graphqlMiddleware(graphqlServer, {
context: async ({ req }) => ({ token: req.headers.token }),
}))
await new Promise(resolve => httpServer.listen({ port: httpPort }, resolve))
console.log(`Server listening on http://localhost:${httpPort}`)Usage (schema & resolver)
# app/schema.graphql
type Query {
jobs: [Job]
}
type Job {
uuid: String
title: String
}// app/resolvers.ts
type AppContext = { userToken: string }
export const resolvers: GraphQLResolverMap<AppContext> = {
Query: {
jobs: (source, args, { userToken }, info) => {
authCheck(userToken)
return jobs
},
},
}Context function & value
Context function passed to the start server helpers are essential to keep user navigation scope under control.
the ideal/recommended way of implementing context function with @occupop/lib-container is to create a scoped
container and extract the user data from request (token, headers, User, etc) to be passed to context. This way,
graphql-resolvers and container-resolvers will receive those injected values to be treated as necessary.
Some usage notes about context signature:
- Treat graphql-resolvers as routers, they should know how to call an useCase/controller.
- Treat useCases and Controllers as orchestrators they should treat/validate input, user/auth and call appropriate services.
- Treat Services as business rules dealer, they know how to deal with its own minimum responsibilities
but not aware of who called. Remember:
- Services can always be called from queues/commands/events).
- Clean code: if a service is doing too much, maybe he is bad designed.
Sample (real life) context function
import { container } from '.container'
export type AppContext = {
container: typeof container // for router scoped resolve only...
}
export async function contextFunction(req, res): AppContext {
const scoped = container.createScope()
const { authService } = container.cradle
const token = req.headers?.Authorization
scoped.register({ authUser: asValue(await authService.getUser(token)) })
return { container: scoped }
}Schema helper
The schema below will be generated by the server to support federated schamas. If you with your IDE to understand directives and know about the base structure just copy and paste the schema helper below anywhere inside your project structure.
Reference: https://www.apollographql.com/docs/graphos/reference/federation/subgraph-spec
# schema-helpers.graphql
"""
This schema is mocks for IDEs to recognize the federation directives and base structure.
Reference: https://www.apollographql.com/docs/graphos/reference/federation/subgraph-spec
"""
type ZXC1 {thoseTypesAreJustAPlaceholderToAllowUnionToBeMocked:String}
type ZXC2 {thoseTypesAreJustAPlaceholderToAllowUnionToBeMocked:String}
union _Entity = ZXC1 | ZXC2
scalar _Any
scalar FieldSet
scalar link__Import
scalar federation__ContextFieldValue
scalar federation__Scope
scalar federation__Policy
enum link__Purpose {
"""
`SECURITY` features provide metadata necessary to securely resolve fields.
"""
SECURITY
"""
`EXECUTION` features provide metadata necessary for operation execution.
"""
EXECUTION
}
type _Service {
sdl: String!
}
extend type Query {
_entities(representations: [_Any!]!): [_Entity]!
_service: _Service!
}
directive @external on FIELD_DEFINITION | OBJECT
directive @requires(fields: FieldSet!) on FIELD_DEFINITION
directive @provides(fields: FieldSet!) on FIELD_DEFINITION
directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
directive @link(url: String!, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
directive @shareable repeatable on OBJECT | FIELD_DEFINITION
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
directive @override(from: String!) on FIELD_DEFINITION
directive @composeDirective(name: String!) repeatable on SCHEMA
directive @interfaceObject on OBJECT
directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
directive @requiresScopes(scopes: [[federation__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
directive @policy(policies: [[federation__Policy!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION
#directive @fromContext(field: ContextFieldValue) on ARGUMENT_DEFINITION
# This definition is required only for libraries that don't support
# GraphQL's built-in `extend` keyword
directive @extends on OBJECT | INTERFACE