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-codegenwe 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.
Comments
Join the discussionNo comments yet. Be the first to comment.
Pure DI in TypeScript Node.js
In this series, I'm covering the topic of performant framework-less Pure DI in Node.Js. Use cases and examples included.
