Skip to content

GraphQL API

Apollo Server v3 ships out of the box at /graphql/. The framework walks the schema registry once at boot (and on every change in dev), composes a Mongoose-derived TC per schema via graphql-compose-mongoose, wraps every resolver in utils/scopeResolver.js, and serves the result.

For schema path: 'account':

OperationResolver name
Find one by idaccountById(_id)
Find many by idsaccountByIds(_ids)
Find one by filteraccountOne(filter, sort)
Find many by filteraccountMany(filter, sort, limit, skip)
CountaccountCount(filter)
Connection (Relay)accountConnection(filter, ...)
PaginationaccountPagination(filter, page, perPage)
SearchaccountSearch(q, filter, sort) (when any field is searchable)
HistoryaccountHistory(_id) (audit-enabled schemas)
Aggregationsaccount<Aggregation>(args) per declared aggregation
Create oneaccountCreateOne(record)
Create manyaccountCreateMany(records)
Update by idaccountUpdateById(_id, record)
Update one by filteraccountUpdateOne(filter, record)
Update many by filteraccountUpdateMany(filter, record)
Remove by idaccountRemoveById(_id)
Remove manyaccountRemoveMany(filter)
RestoreaccountRestore(_id) (soft-delete-enabled schemas)
Transition<path>Transition<Field>(_id, to) per state-machine field — to is typed as the schema’s generated enum

Every resolver is wrapped via the helpers in utils/scopeResolver.js:

WrapperUse for
wrapFilterRead-many resolvers (Many, Connection, Pagination, Count, Search).
wrapFindById / wrapFindByIdsRead-by-id resolvers.
wrapCreateOne / wrapCreateManyCreate resolvers — stamps userId / accountId.
wrapByIdMutationUpdate / remove / restore by id.

userId: ctx.user.user_id is injected into the filter before the resolver runs. If you write a custom resolver, wrap it. Going direct to a Mongoose model bypasses tenant scoping.

const { wrapFilter } = require('./utils/scopeResolver');
tc.addResolver({
name: 'accountsWithDeals',
resolve: wrapFilter(
{ schema, kind: 'read' },
async (rp, ctx) => {
// rp.args.filter has userId injected; query freely.
}
),
});

Three input types per schema, generated automatically:

TypeExcludes
<Path>InputComputed fields, file fields, server-stamped fields (userId/accountId). Used for CreateOne.
<Path>UpdateInputSame exclusions, plus all required fields are nullable. Used for partial updates.
<Path>FilterInputAll readable fields, with mongo-querystring operators.

Input types deliberately omit ownership fields so a client can’t supply them — the wrappers stamp them server-side.

Bearer JWT — exactly the same as REST. Apollo Server’s request context picks up Authorization: Bearer ..., verifies it against TOKEN_KEY, and exposes ctx.user with { user_id, email, roles }.

context: ({ req }) => ({
user: req.user, // populated by auth middleware
// ... other things resolvers might need
}),

Resolvers without auth context are rejected before they run via wrapFilter’s built-in check.

GraphQL list resolvers honour the same deletedAt: null predicate as REST. To include tombstones, pass _includeDeleted: true on the Many / One / Count resolvers — same flag as __includeDeleted on the REST surface.

accountRemoveById performs a soft-delete by default. accountRestore clears the tombstone. accountRemoveOne / RemoveMany follow the same pattern.

Relations declared in the schema’s relations map appear as nested fields:

{
accountById(_id: "abc") {
name
contacts(filter: { /* ... */ }) {
_id, name
}
primaryContact { _id, name }
}
}

hasMany relations accept a filter argument; hasOne / belongsTo are scalar.

State-machine fields surface as a literal enum in the GraphQL output type, plus a dedicated transition mutation per field. The mutation runs the same validate / persist / audit / event / onEnter pipeline as the REST PUT path, and to is typed as the schema’s generated enum so a typo on the wire is caught before any handler runs:

mutation {
quoteTransitionStatus(_id: "abc", to: approved) {
record {
_id
status # enum value
availableTransitions {
status # [String!]
}
}
}
}

Updating the state-machine field through the standard <path>UpdateById resolver also validates against the state machine — the dedicated transition mutation is the preferred call shape, but a regular update can’t bypass the transition graph.

INVALID_TRANSITION errors carry the structured payload in errors[0].extensions for clients to react to.

Apollo Server v3 builds its schema at construction time — there’s no “swap the schema” API. The framework solves it with an indirection middleware: the parent app holds a pointer the loader can swap on rebuild. In-flight requests hit the previous router; new requests hit the new one. See Hot reload.

In dev (NODE_ENV !== 'production'), the GraphQL playground is mounted at /graphql/. Introspection is also gated on dev.

In production, both are off. If you need GraphQL introspection in production, override the introspection flag in app.js.