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
$ 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:
- Take client requests with a Raw endpoint.
- Pass along the request object (body, headers, query params, etc.) to the GraphQL library.
- Use the GraphQL library to handle the queries and mutations.
- 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();
},
);
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:
- 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).
- 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.
- Testing - You can easily test your resolvers by mocking the API calls.
- 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.graphqltype 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.tsimport { 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 };
},
);