Skip to main content

GraphQL

Refract’s API is GraphQL end to end: Apollo Server on the backend, Apollo Client in the portal, and a small codegen loop so the frontend never guesses field shapes. Resolvers stay thin; utilities own the rules; the schema file is generated so humans and agents share one contract. Once you internalize that pipeline, adding a query or mutation becomes a boring checklist—which is exactly what you want at 2 a.m. when shipping a hotfix.

How it works

  1. You write a resolver in apps/backend/src/gql/.
  2. Running make gql-codegen dumps the full schema to /shared/template.schema.graphql.
  3. The frontend codegen reads that schema plus any useQuery / useMutation calls in the frontend source, and generates apps/portal/src/gql/hooks.ts — a fully typed file of React hooks.
  4. Your React component imports and calls the generated hook. TypeScript ensures the component’s types stay in sync with the API.
The CI codegen-verification job and make local-verification both enforce this — if you forget to run codegen after a schema change, they fail and tell you to commit the updated generated file.

Resolver structure

Resolvers in Refract follow a consistent shape. Here’s a real example — a query that fetches countries with Redis caching:
Each resolver file exports:
  • The resolver function (named after the operation, e.g. fetchCountries)
  • A schema builder function (named build<OperationName>Schema)
  • The parameter type, exported so tests can import it instead of redefining it

Middleware chain

Resolvers use resolveWithMiddlewares to enforce authentication and authorization before running business logic. A typical secured mutation looks like:
The middleware array runs in order. If any middleware rejects (e.g. user isn’t logged in), the chain stops and the error is returned to the client. Your business logic lives at the end of the chain.

Mutation return types

Mutations always return a discriminated union:
This makes it straightforward for the frontend to handle success and failure without throwing exceptions. On success, you can add a result field with whatever data the client needs.
⚠️ Watch out: the resolver’s return shape must match the GraphQL SDL response fields exactly. For example, if your resolver returns reason: string, your SDL must type reason as GraphQLString (not a list).

Adding a new query or mutation

  1. Create the resolver file For a query: apps/backend/src/gql/queries/fetchMyThing.ts For a mutation: apps/backend/src/gql/mutations/createMyThing.ts Export the resolver function, the schema builder, and the params type:
  2. Add business logic to a utility apps/backend/src/utilities/myThing.ts — pure function, no direct SDK calls:
    💡 Tip: for input normalization and validation, keep pure helpers colocated with the operation (e.g. inside apps/backend/src/gql/mutations/<operationName>.ts) or in stable apps/backend/src/utilities/*. Avoid importing helpers from a mutation folder you plan to delete during extraction.
  3. Register the schema builder In apps/backend/src/gql/index.ts, add your builder to the merged schema:
    💡 Tip: schema registration is a hard gate. Don’t delete or rename the previous operation module until you’ve updated apps/backend/src/gql/index.ts and make gql-codegen succeeds.
  4. Run codegen
    This generates the updated apps/portal/src/gql/hooks.ts with a new useFetchMyThingQuery hook.
  5. Use the hook in the frontend
  6. Write a test apps/backend/src/gql/queries/__tests__/fetchMyThing.spec.ts:
  7. Verify
    💡 Tip: after running make gql-codegen, sanity-check apps/shared/template.schema.graphql to ensure your operation field names (e.g. updateMailingListContact) appear exactly once. This catches accidental SDL duplication that unit tests alone might not reveal.

What’s next?