@nwire/apollo
v0.12.1
Published
Nwire — Apollo Server (v4) interop adapter. actionResolver(action) plugs a Nwire ActionDefinition into a GraphQL schema as a field resolver; nwireApolloContext({ runtime }) adds runtime.dispatch + envelope to Apollo's per-request context; mountNwireOnApol
Readme
@nwire/apollo
Apollo Server (v4) interop — plug Nwire actions into a GraphQL schema as field resolvers; reuse a hand-authored schema you already maintain.
What it is
A thin adapter (~60 LOC of real code). actionResolver(action) wraps a Nwire ActionDefinition so it serves as a GraphQL field resolver. nwireApolloContext({ runtime }) injects the runtime + a fresh envelope onto every request. mountNwireOnApollo(...) bundles the context factory with Nwire-aware error formatting so defineError errors surface with stable extensions.code.
Same migration story Express got: keep the schema and gateway you already have, stop hand-writing resolver bodies that just call services.
A native GraphQL transport (schema generated from actions/queries automatically, subscriptions on top of
@nwire/bus) is a separate roadmap item. This package is for teams already running Apollo with a hand-authored schema. Cross-link:@nwire/express.
Install
pnpm add @nwire/apollo @apollo/server graphql@apollo/server (^4) and graphql (^16) are peer deps — bring whatever versions your Apollo setup already uses. Apollo v3 is EOL and unsupported.
Quickstart
import { z } from "zod";
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import gql from "graphql-tag";
import { defineAction, Runtime } from "@nwire/forge";
import { actionResolver, mountNwireOnApollo } from "@nwire/apollo";
const submitAnswer = defineAction({
name: "submissions.submit-answer",
schema: z.object({ questionId: z.string(), answer: z.string() }),
handler: async (input, ctx) => ({
id: "sub-1",
...input,
student: ctx.envelope.userId,
}),
});
const runtime = new Runtime();
runtime.registerHandler(submitAnswer.handler!);
const typeDefs = gql`
input SubmitAnswerInput {
questionId: String!
answer: String!
}
type Submission {
id: ID!
questionId: String!
answer: String!
student: String
}
type Mutation {
submitAnswer(input: SubmitAnswerInput!): Submission!
}
type Query {
_: Boolean
}
`;
const apollo = new ApolloServer({
typeDefs,
resolvers: {
Mutation: { submitAnswer: actionResolver(submitAnswer) },
},
...mountNwireOnApollo({ runtime }),
});
await startStandaloneServer(apollo, { listen: { port: 4000 } });actionResolver reads args.input by default (matches GraphQL convention for mutation inputs); customise via extractInput for query-style flat args. Dispatched actions return their handler value verbatim to the GraphQL response — emit events, no events, return a plain object, whatever the handler does.
API
actionResolver(action, options?)—ActionDefinition→ GraphQL field resolver. Options:extractInput,extractEnvelope,resolveRuntime.nwireApolloContext({ runtime, extractEnvelope?, extractUser? })— Apollocontextfactory that injectsruntime+ envelope per request.mountNwireOnApollo({ runtime, ... })— convenience bundle: returns{ context, formatError }you spread intoApolloServerconstructor +startStandaloneServeroptions.formatNwireError(formattedError, rawError)— standaloneformatErrorcallback. Use directly if you compose your own error formatter.
Errors
When a handler throws a defineError-style value (e.g. QuestionLocked), GraphQL clients receive:
{
"errors": [
{
"message": "Question is locked for further submissions.",
"extensions": { "code": "QUESTION_LOCKED", "status": 423 }
}
]
}Same stable code the REST transport emits — clients can branch on extensions.code and ignore wire format.
See also
@nwire/express— same architectural pattern for Express interop.docs/recipes/apollo-interop.md— full recipe with auth header extraction + per-resolver overrides.
