Introduction
This is part 2 of our tutorial series. In the previous tutorial we setup our project and using graphql-yoga
created a Todos GraphQL server covering all the CRUD operations. In this tutorial we will be using graphql-modules
and modularize our codebase.
Overview
I've seen code bases where people club all the controllers under controllers folder, routes under routes, models under models folder. Similarly, for GraphQL projects they club all resolvers under resolvers and schemas under schema folder. I like to club my files by features, so I will create a todos
folder, it will contain todos.controller.ts, todos.model.ts, todos.router.ts
, etc. In this tutorial we will club our files by features.
This series is not recommended for beginners some familiarity and experience working with Nodejs
, GraphQL
& Typescript
is expected. In this tutorial we will cover the following : -
- Setup the folder structure.
- Create Todos schema and resolvers.
- Create Comments schema and resolvers.
- Run the Server and Test it.
All the code for this tutorial is available under the graphql-modules
branch, check the repo.
Step One: Folder Setup
First, we will create a modules
folder and inside the modules
folder create 2 more folders todos & comments
. Inside each of these folders create resolver.ts
, a schmea.graphql
and an entry index.ts
file. We are basically grouping files by features -
From your terminal install the following dependencies -
yarn add graphql-modules graphql-import-node @envelop/graphql-modules
-
graphql-modules
, will help us in modularizing our codebase and separate our GraphQL schema into manageable small bits & pieces. - We will be using
.graphql
extension for schema files. To work with.graphql
files in a Nodejs project we usegraphql-import-node
package. - Finally, to make our modules work with
graphql-yoga
we use the@envelop/graphql-modules
package.
Step Two: Create Todos Schema & Resolvers
Now we will move the schema and resolvers that we created for todos & comments
in the previous tutorial to their respective folders. First inside modules/todos/schema.graphql
paste the schema -
enum TaskStatus {
PENDING
IN_PROGRESS
DONE
}
type Todo {
id: String!
task: String!
description: String!
status: TaskStatus!
tags: [String]!
comments: [Comment]
createdAt: String!
updatedAt: String!
}
type Query {
todos: [Todo]!
}
input TodoInput {
task: String!
description: String!
status: TaskStatus!
tags: [String]
}
input EditTodoInput {
id: ID!
task: String!
description: String!
status: TaskStatus!
tags: [String]
}
input DeleteTodoInput {
id: ID!
}
type Mutation {
addTodo(input: TodoInput): Todo!
editTodo(input: EditTodoInput): Todo!
deleteTodo(input: DeleteTodoInput): Todo!
}
Notice that all the types, inputs, queries & mutations related to the Todos
type have been moved here. Similarly lets move our resolvers for Todos
to modules/todos/resolvers.ts
-
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { GraphQLError } from "graphql";
import { Todos } from "@prisma/client";
import { GraphQLContext } from "../../context";
export const todosResolvers = {
Query: {
todos(parent: unknown, args: {}, context: GraphQLContext) {
return context.prisma.todos.findMany();
},
},
Todo: {
comments(parent: Todos, args: {}, context: GraphQLContext) {
return context.prisma.comments.findMany({
where: {
todoId: parent.id,
},
});
},
},
Mutation: {
addTodo(parent: unknown, args: { input: Todos }, context: GraphQLContext) {
return context.prisma.todos.create({
data: {
task: args.input.task,
description: args.input.description,
status: args.input.status,
tags: args.input.tags,
},
});
},
async editTodo(
parent: unknown,
args: { input: Todos },
context: GraphQLContext
) {
const todoId = args.input.id;
try {
const todo = await context.prisma.todos.update({
data: {
task: args.input.task,
description: args.input.description,
status: args.input.status,
tags: args.input.tags,
},
where: {
id: todoId,
},
});
return todo;
} catch (error) {
if (
error instanceof PrismaClientKnownRequestError &&
error.code == "P2003"
) {
return Promise.reject(
new GraphQLError(
`Cannot delete a non-exiting todo with id ${todoId}`
)
);
}
return Promise.reject(error);
}
},
async deleteTodo(
parent: unknown,
args: { input: Pick<Todos, "id"> },
context: GraphQLContext
) {
const todoId = args.input.id;
try {
const todo = await context.prisma.todos.delete({
where: {
id: todoId,
},
});
return todo;
} catch (error) {
if (
error instanceof PrismaClientKnownRequestError &&
error.code == "P2003"
) {
return Promise.reject(
new GraphQLError(
`Cannot delete a non-exiting todo with id ${todoId}`
)
);
}
return Promise.reject(error);
}
},
},
};
Finally under modules/todos/index.ts
file we create our module -
import { createModule } from "graphql-modules";
import * as TodosSchema from "./schema.graphql";
import { todosResolvers } from "./resolvers";
export const todosModule = createModule({
id: "todos-module",
dirname: __dirname,
typeDefs: [TodosSchema],
resolvers: todosResolvers,
});
You might be getting a type error for the ./schema.graphql
import, to resolve this add the following import at the top of src/index.ts
-
import "graphql-import-node";
Step Three: Create Comments Schema & Resolvers
Lets repeat Step Two, this time for the comments module, under modules/commeants/schema.graphql
paste the following -
type Comment {
id: ID!
body: String!
}
type Query {
comment(id: ID!): Comment!
}
input CommentInput {
todoId: ID!
body: String!
}
type Mutation {
postCommentOnTodo(input: CommentInput): Comment!
}
Under modules/comments/resolvers.ts
paste -
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { GraphQLError } from "graphql";
import { GraphQLContext } from "../../context";
export const commentsResolvers = {
Query: {
comment(parent: unknown, args: { id: string }, context: GraphQLContext) {
return context.prisma.comments.findUnique({
where: {
id: args.id,
},
});
},
},
Mutation: {
async postCommentOnTodo(
parent: unknown,
args: { input: { todoId: string; body: string } },
context: GraphQLContext
) {
const todoId = args.input.todoId;
try {
const comment = await context.prisma.comments.create({
data: {
todoId,
body: args.input.body,
},
});
return comment;
} catch (error) {
if (
error instanceof PrismaClientKnownRequestError &&
error.code == "P2003"
) {
return Promise.reject(
new GraphQLError(
`Cannot comment on a non-exiting todo with id ${todoId}`
)
);
}
return Promise.reject(error);
}
},
},
};
Finally, under modules/comments/index.ts
create the comments module -
import { createModule } from "graphql-modules";
import * as CommentsSchema from "./schema.graphql";
import { commentsResolvers } from "./resolvers";
export const commentsModule = createModule({
id: "comments-module",
dirname: __dirname,
typeDefs: [CommentsSchema],
resolvers: commentsResolvers,
});
Step Four: Run the Server & Test it
Now that we have created our modules and modularized our codebase, lets stitch it with graphql-yoga
build it and test it. Under modules/index.ts
lets create an application from all our modules -
import { createApplication } from "graphql-modules";
import { todosModule } from "./todos";
import { commentsModule } from "./comments";
export const application = createApplication({
modules: [todosModule, commentsModule],
});
Now in the src/index.ts
file lets import this application and stitch it with graphql-yoga
-
import "graphql-import-node";
import { createServer } from "http";
import { createYoga } from "graphql-yoga";
import { useGraphQLModules } from "@envelop/graphql-modules";
import { createContext } from "./context";
import { application } from "./modules";
function main() {
const yoga = createYoga({
schema: application.schema,
context: createContext,
plugins: [useGraphQLModules(application)],
});
const server = createServer(yoga);
server.listen(4000, () => {
console.log("Server started on Port no. 4000");
});
}
main();
We use the @envelop/graphql-modules
package and pass in our application, we also pass the application.schmea
.
From your terminal run yarn dev
, you will see an error Cannot find module .schema.graphql
. Check your build output in the dist
directory it has no .graphql
files, because esbuild-tsc
will only compile TypeScript files to JavaScript. We need to copy our .graphql
files into our build folder. To do so first install cpy
as a dev dependency -
yarn add -D cpy
Then from the root of the project create etsc.config.js
-
module.exports = {
esbuild: {
minify: false,
target: "es2016",
},
postbuild: async () => {
const cpy = (await import("cpy")).default;
await cpy(
[
"src/**/*.graphql", // Copy all .graphql files
"!src/**/*.{tsx,ts,js,jsx}", // Ignore already built files
],
"dist"
);
},
};
Now from the terminal run yarn dev
, navigate to localhost:4000/graphql in the browser and play around with the GraphQL API.
Conclusion
We successfully modularized our code base using graphql-modules
and created a simple GraphQL API following the schema-first
approach. In the next tutorial we will take a look at code-first
approach to developing a GraphQL API using graphql-pothos
. Until then PEACE.
Top comments (0)