Back to Encore

GraphQL

docs/ts/primitives/graphql.mdx

1.56.94.9 KB
Original Source

Encore.ts has great support for GraphQL with its type-safe approach to building APIs.

Encore's automatic tracing also makes it easy to find and fix performance issues that often arise in GraphQL APIs (like the N+1 problem).

<GitHubLink href="https://github.com/encoredev/examples/tree/main/ts/graphql" desc="Using Apollo GraphQL together with Encore.ts." />

Concept

To serve a GraphQL API, you can leverage Raw endpoints. Raw endpoints provide direct access to the underlying HTTP request and response objects, enabling integration with a GraphQL library. Below is an outline of the high-level steps required for setup:

  1. Take client requests with a Raw endpoint.
  2. Pass along the request object (body, headers, query params, etc.) to the GraphQL library.
  3. Use the GraphQL library to handle the queries and mutations.
  4. Return the GraphQL response from the Raw endpoint.

Which GraphQL library you choose is up to you. It should work any GraphQL library that allows you to pass along the request object and get a GraphQL response object back without having to start a new HTTP server.

Example

Here's an example using Apollo to create a GraphQL API:

ts
import { HeaderMap } from "@apollo/server";
import { api } from "encore.dev/api";
const { ApolloServer, gql } = require("apollo-server");
import { json } from "node:stream/consumers";

// Type definition schema
const typeDefs = gql`
  ...
`;

// Resolver functions
const resolvers = {
  // ...
};

const server = new ApolloServer({ typeDefs, resolvers });

await server.start();

export const graphqlAPI = api.raw(
  { expose: true, path: "/graphql", method: "*" },
  async (req, res) => {
    // Make sure the Apollo server is started
    server.assertStarted("/graphql");

    // Extract headers in a format that Apollo understands
    const headers = new HeaderMap();
    for (const [key, value] of Object.entries(req.headers)) {
      if (value !== undefined) {
        headers.set(key, Array.isArray(value) ? value.join(", ") : value);
      }
    }

    // Get response from Apollo server
    const httpGraphQLResponse = await server.executeHTTPGraphQLRequest({
      httpGraphQLRequest: {
        headers,
        method: req.method!.toUpperCase(),
        body: await json(req),
        search: new URLSearchParams(req.url ?? "").toString(),
      },
      context: async () => {
        return { req, res };
      },
    });

    // Set headers
    for (const [key, value] of httpGraphQLResponse.headers) {
      res.setHeader(key, value);
    }

    // Set status code
    res.statusCode = httpGraphQLResponse.status || 200;

    // Write response if it's complete
    if (httpGraphQLResponse.body.kind === "complete") {
      res.end(httpGraphQLResponse.body.string);
      return;
    }

    // Write response in chunks if it's async
    for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
      res.write(chunk);
    }
    res.end();
  },
);

<RelatedDocsLink paths={["/docs/ts/tutorials/graphql"]} />

Call REST APIs in resolvers

It's often a good idea to create REST endpoints for your business logic and let your resolvers forward requests to those endpoints. This has a few benefits:

  1. Getting traces - Calls to Encore endpoints results in traces being created, even for internal API calls. Having traces makes it easy to find and fix performance issues that often arise in GraphQL APIs (like the N+1 problem).
  2. Thin resolvers - By making your REST request/response objects extend the generated GraphQL types, your resolvers will just be thin wrappers around your REST endpoints.
  3. Testing - You can easily test your resolvers by mocking the API calls.
  4. REST & GraphQL - You will have both a REST and GraphQL API.

Here's an example of how it might look like if you can call a REST API from a resolver:

graphql
-- schema.graphql --
type Query {
  books: [Book]
}

type Book {
  title: String!
  author: String!
}
ts
-- resolver.ts --
// Import the book service from the generated service clients
import { book } from "~encore/clients";
import { QueryResolvers } from "../__generated__/resolvers-types";

const queries: QueryResolvers = {
  books: async () => {
    // Call book.list to get the list of books
    const { books } = await book.list();
    return books;
  },
};

export default queries;
ts
-- book.ts --
import { api } from "encore.dev/api";
// Import Book type the generated schema types
import { Book } from "../__generated__/resolvers-types";

const db: Book[] = [
  {
    title: "To Kill a Mockingbird",
    author: "Harper Lee",
  },
  // ...
];

// REST endpoint to get the list of books
export const list = api(
  { expose: true, method: "GET", path: "/books" },
  async (): Promise<{ books: Book[] }> => {
    return { books: db };
  },
);