GraphQL

Serve a GraphQL API under a Raw endpoint

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).

Related example
Using Apollo GraphQL together with Encore.ts.
$ encore app create --example=ts/graphql

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:

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(); }, );
Related docs

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:

schema.graphql
type Query { books: [Book] } type Book { title: String! author: String! }
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;
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 }; }, );