docs/ts/primitives/graphql.mdx
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." />
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:
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.
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();
},
);
<RelatedDocsLink paths={["/docs/ts/tutorials/graphql"]} />
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:
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 };
},
);