Using GraphQL Yoga with Fastify | Pure DI in TypeScript Node.js
In the tutorial preceding this one, I demonstrated how to create a logging service with metadata. In this article, I will be implementing a new GraphQL endpoint for our application.
I will use the repo as starting point.
Data Store
Before we start implementing the GraphQL server, let's make changes to our business logic layer by adding a new implementation for FindOrderById
.
// order-store.ts
interface OrderStore {
find(id: ID): Promise<Order | undefined>;
}
// find-order-by-id-from-store-provider.ts
export class FindOrderByIdFromStoreProvider {
constructor(private readonly store: OrderStore) {}
findOrderById: FindOrderById = (orderId) => {
return this.store.find(orderId);
};
}
// registry/find-order-by-id-from-store.ts
export function findOrderById() {
return ({ orderStore }: { orderStore: OrderStore }) => {
const { findOrderById } = new FindOrderByIdFromStoreProvider(orderStore);
return {
findOrderById,
};
};
}
In the code above I also introduced OrderStore
interface. We can have multiple implementations for the interface, such as one making requests to SQL DB, or another making HTTP requests to a server.
Essentially, the FindOrderById
is our business logic primary
port, while OrderStore
is a secondary
port, in terms of hexagonal architecture.
For now, let's implement the stub adapter for OrderStore
:
// stub-order-store.ts
export class StubOrderStore implements OrderStore {
constructor(private readonly logger: Logger) {}
find(id: ID): Promise<Order | undefined> {
this.logger.info(`calling StubOrderStore.find with orderId: ${id}`);
return Promise.resolve({
id,
});
}
}
// registry/stub-order-store.ts
export function orderStore() {
return ({ createLogger }: { createLogger: CreateLogger }) => {
const orderStore = new StubOrderStore(createLogger(StubOrderStore.name));
return {
orderStore,
};
};
}
// create-app-registry.ts
export function createAppRegistry() {
return (
new RegistryComposer()
// ...
.add(orderStore())
.add(findOrderById())
// ...
.compose()
);
}
GraphQL Server
Since I was using fastify
I'm going to integrate it with GraphQL Yoga because of simple integration.
First of all, we need to install some dependencies:
yarn install graphql graphql-yoga dataloader
yarn install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers ts-node
Now we can add graphqlRoutes
FastifyPlugin
:
// routes/graphql.ts
type HandleNodeRequest = (
nodeRequest: NodeRequest
) => Promise<Response> | Response;
export function graphqlRoutes({
handleGraphQLNodeRequest,
}: {
handleGraphQLNodeRequest: HandleNodeRequest;
}): FastifyPluginCallback {
return (fastify, _, next) => {
fastify.route({
url: "/graphql",
method: ["GET", "POST", "OPTIONS"],
handler: async (req, reply) => {
const response = await handleGraphQLNodeRequest(req);
response.headers.forEach((value, key) => {
reply.header(key, value);
});
reply.status(response.status);
reply.send(response.body);
return reply;
},
});
next();
};
}
Here we added a simple route for any HTTP method, that just forwards the request to graphqlYoga.handleNodeRequest
.
Let's create the route registrar now:
// registry/graphql-yoga.ts
export function graphqlYoga<
TServerContext extends Record<string, any> = object,
TUserContext extends Record<string, any> = object
>(options?: Omit<YogaServerOptions<TServerContext, TUserContext>, "schema">) {
return ({
createLogger,
graphqlSchema,
}: {
createLogger: CreateLogger;
graphqlSchema: GraphQLSchemaWithContext<TUserContext & TServerContext>;
}) => {
const graphqlYoga = createYoga<TServerContext, TUserContext>({
...options,
logging: createLogger("GraphQLYoga"),
schema: graphqlSchema,
});
return {
handleGraphQLNodeRequest: graphqlYoga.handleNodeRequest.bind(graphqlYoga),
};
};
}
and add it to our fastifyServer
:
// registry/fastify-server.ts
export function fastifyServer() {
return (
deps: Parameters<typeof runWithRequestIdPlugin>[0] &
Parameters<typeof ordersRoutes>[0] &
Parameters<typeof graphqlRoutes>[0]
) => {
const server = Fastify({});
server.register(runWithRequestIdPlugin(deps));
server.register(ordersRoutes(deps));
server.register(graphqlRoutes(deps));
return {
fastifyServer: server,
};
};
}
GraphQL Schema
Now we can register our GraphQL schema:
# schema.gql
type Order {
id: ID!
}
type Query {
order(id: ID!): Order
}
Using
graphql-codegen
we can generate the typescript definitions from the schema file, see config here.
// graphql-schema.ts
import { Resolvers } from "./generated/resolvers-types";
export function graphqlSchema() {
return ({ findOrderById }: { findOrderById: FindOrderById }) => {
return {
graphqlSchema: createSchema({
typeDefs: fs.readFileSync(
path.join(__dirname, "../schema.gql"),
"utf8"
), // <-- I read schema from file here, but we can also use [esbuild loader](https://github.com/luckycatfactory/esbuild-graphql-loader)
resolvers: {
Query: {
order: (_, args) => findOrderById(args.id),
},
} as Resolvers,
}),
};
};
}
// create-app-registry.ts
export function createAppRegistry() {
return (
new RegistryComposer()
// ...
.add(graphqlSchema())
.add(graphqlYoga())
/// ...
.compose()
);
}
// index.ts
async function main() {
const registry = createAppRegistry();
const server = Fastify({});
server.register(runWithRequestIdPlugin(registry));
server.register(ordersRoutes(registry));
server.register(graphqlRoutes(registry));
try {
await server.listen({ port: PORT });
console.log("listening on port", PORT);
} catch (err) {
server.log.error(err);
process.exit(1);
}
}
Let's run the app now and try to hit our new /graphql
endpoint
POST /graphql HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Content-Length: 80
{"query":"query {\n order(id: \"order-0\") {\n id\n }\n}","variables":{}}
Response:
{
"data": {
"order": {
"id": "order-0"
}
}
}
Full example source code can be found here
Conclusion
In this article, we learned how to add the GraphQL Yoga
plugin to Fastify
using a Pure Dependency Injection registry.
In the next article, I'll cover a more interesting topic: how to use DataLoader
with GraphQL Yoga
and AsyncLocalStorage
.