Apollo Server interop
Use
@nwire/apolloto plug Nwire actions into an existing Apollo Server schema as field resolvers. Same migration story as Express — keep the schema, drop the boilerplate resolvers.
When to use this
You already run Apollo (gateway, federation, persisted queries, custom directives — anything) and you want to start adopting Nwire one resolver at a time. If you're greenfield-building GraphQL, watch for the native transport in @nwire/http instead.
Install
pnpm add @nwire/apollo @apollo/server graphqlApollo Server v4 + GraphQL ^16 are peer deps. Apollo v3 is EOL and not supported.
The 30-second example
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",
input: z.object({ questionId: z.string(), answer: z.string() }),
handler: async (ctx) => {
const { input } = ctx;
return {
id: "sub-1",
...input,
student: ctx.envelope.userId,
};
},
});
const runtime = new Runtime();
runtime.registerHandler(submitAnswer);
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 } });That's it. The GraphQL field submitAnswer is now implemented by the Nwire action — same handler that runs from the REST transport, the queue worker, or the CLI.
Pulling user / tenant out of the HTTP request
mountNwireOnApollo accepts an extractEnvelope callback. Apollo's standalone server passes { req, res } to the context factory:
import { seedEnvelope } from "@nwire/envelope";
import { mountNwireOnApollo } from "@nwire/apollo";
const apollo = new ApolloServer({
typeDefs,
resolvers,
...mountNwireOnApollo<{ req: import("http").IncomingMessage }>({
runtime,
extractEnvelope: ({ req }) =>
seedEnvelope({
userId: req.headers["x-user-id"] as string | undefined,
tenant: req.headers["x-tenant-id"] as string | undefined,
}),
}),
});When Apollo runs as Express middleware (@apollo/server/express4), use the same shape — the context arg is { req, res } from Express.
Error contract
@nwire/forge ships defineError(...) — a stable, throwable error value with a code, status, and summary:
import { defineError } from "@nwire/forge";
export const QuestionLocked = defineError({
code: "QUESTION_LOCKED",
status: 423,
summary: "Question is locked for further submissions.",
});A handler throw QuestionLocked surfaces over GraphQL as:
{
"errors": [{
"message": "Question is locked for further submissions.",
"extensions": { "code": "QUESTION_LOCKED", "status": 423 }
}]
}mountNwireOnApollo registers formatNwireError for you. Clients can branch on extensions.code — the same code the REST transport sets in its JSON error envelope.
Per-resolver overrides
actionResolver accepts an options bag:
actionResolver(listSubmissions, {
// Args shape doesn't match the default `input` envelope.
extractInput: (args) => ({ studentId: args.studentId, status: args.status }),
// Sometimes the runtime lives on a sub-object of the GQL context.
resolveRuntime: (ctx) => ctx.deps.runtime,
})When the defaults don't fit, override one knob at a time. Everything else stays in the action — the resolver is still a one-liner.
See also
- Migrating from Express — same idea, different transport.
@nwire/express— Express adapter (toExpress(api)/fromExpress(mw)).defineError— stable error contract across REST / GraphQL / CLI.