Introduction
Back in 2019 - 2020 I used to build GraphQL servers using apollo-server
or postgraphile
, today we have many options to build a GraphQL server. There are popular tools like Hasura, Grafbase and WunderGraph that can give you a GraphQL endpoint for your data sources, or you can create one from scratch either by using a schema-first
or a code-first
approach. If you pick the latter option, this tutorial series can serve as a good starting point. We will be building a simple Todo GraphQL server using Node.js, Typescript, Prisma and glance over the schema-first & code-first approaches
: -
- In the first tutorial we will be creating a GraphQL server using Typescript and
graphql-yoga
followingschema-first
approach. - In the second tutorial we will modularize our
schema-first
codebase usinggraphql-modules
. - In the third tutorial we will be using
graphql-pothos
to create acode-first
GraphQL server. - In the last tutorial we will build a frontend app using React,
urql
and codegen.
Overview
This series is not recommended for beginners some familiarity and experience working with Nodejs
, GraphQL
& Typescript
is expected. In this post which is part one of our series we will start from scratch, we will cover the following : -
- Bootstrap the project set up
esbuild
&nodemon
. - Setup
graphql-yoga
with a dummy schema. - Setup Prisma for querying the database.
- Build the Todos GraphQL schema & resolvers.
All the code for this tutorial is available under the schema-first
branch, check the repo.
Step One: Bootstrap the project
First create a new folder and from the terminal run npm init
-
mkdir todos-graphql-server
cd todos-graphql-server
npm init
After you finish executing npm init command you have the package.json file in your project. Now let us install some dev dependencies run the following in your terminal : -
yarn add -D typescript @types/node nodemon
From the root of our project create a new file tsconfig.json
-
{
"compilerOptions": {
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"module": "commonjs" /* Specify what module code is generated. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
Given the fact we are using Typescript, we need to build our project in order to run it, in my previous node tutorial series I used ts-node
, this time we will use esbuild
for faster build times. From your terminal -
yarn add -D esbuild esbuild-node-tsc
Now that we have installed esbuild dependencies, lets set up the build script for building the project. From the root of the project create a new file nodemon.json
-
{
"watch": ["src"],
"ignore": ["src/**/*.test.ts"],
"ext": "ts, mjs, js, json, graphql",
"exec": "etsc && node -r ./dist/index.js",
"legacyWatch": true
}
Whenever we save the changes done to .ts, .js, .graphql
files under src
folder we will run the etsc
command that will build our project, the output of which would be in the dist
folder, we then run the ./dist/index.js
file.
Finally, inside the package.json
file add the following under the scripts section -
"scripts": {
"dev": "nodemon",
"start": "node dist/src/index.js"
},
From the root of the project create a .gitignore
file -
node_modules
.env
dist
Step Two: Setup graphql-yoga
Create a src
folder and an src/index.ts
file. Run yarn dev
from the terminal and try writing some code under the index.ts
file, nodemon should compile the code on every file save. From the terminal install the following dependencies -
yarn add graphql graphql-yoga
Now let us create a simple GraphQL schema and test it, under src/index.ts
file paste the following code -
import { createServer } from 'http';
import { createSchema, createYoga } from 'graphql-yoga';
const schema = createSchema({
typeDefs: `
type Query {
hello: String
}
`,
resolvers: {
Query: {
hello: () => 'Hello from GraphQL',
},
},
});
function main() {
const yoga = createYoga({ schema });
const server = createServer(yoga);
server.listen(4000, () => {
console.log('Server started on Port: 4000');
});
}
main();
From the terminal run yarn dev
in your browser navigate to http://localhost:4000/graphql
and paste the following in the query tab and run it -
Instead of writing the schema in the main index.ts
file, we will create a new file src/schema.ts
-
import { createSchema } from 'graphql-yoga';
const typeDefinitions = `
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 Comment {
id: ID!
body: String!
}
type Query {
todos: [Todo]!
}
`;
const resolvers = {
Query: {
todos() {
return [
{
id: '1',
task: 'Learn GraphQL Server',
description: 'Develop a GraphQL Server',
status: 'PENDING',
tags: ['node', 'graphql'],
comments: [],
createdAt: '5/25/20203',
updatedAt: '5/25/2023',
},
];
},
},
};
export const schema = createSchema({
typeDefs: [typeDefinitions],
resolvers: [resolvers],
});
We created a simple Todo schema with a todo
query and resolved the todo
query. Finally, under src/index.ts
paste the following -
import { createServer } from 'http';
import { createYoga } from 'graphql-yoga';
import { schema } from './schema';
function main() {
const yoga = createYoga({ schema });
const server = createServer(yoga);
server.listen(4000, () => {
console.log('Server started on Port: 4000');
});
}
main();
Save the file, navigate to the GraphiQL and verify the following todos query -
Step Three: Setup Prisma
In this section we will be connecting to and querying the database. In my case, I'll be using neon db service for my database. From your terminal run -
yarn add -D prisma
Next we have to run npx prisma init
from the terminal this will create a prisma
folder and a schema.prisma
file inside it. In the primsa/schema.prisma
file paste the schema for this tutorial -
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
enum TaskStatus {
PENDING
IN_PROGRESS
DONE
}
model Todos {
id String @id @default(uuid())
task String
description String
status TaskStatus
tags String[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
Comments Comments[]
}
model Comments {
id String @id @default(uuid())
body String
todo Todos @relation(fields: [todoId], references: [id], onDelete: Cascade)
todoId String
}
Make sure you add the DATABASE_URL
key in the env, I am also using a SHADOW_DATABASE_URL
with neon db.
From the terminal run npx prisma migrate dev --name init
, this will create our tables in the database and also generate a type safe prisma client, with which we can query the database.
Step Four: Creating Todos GraphQL Schema & Resolvers
First, we will create our prisma client and pass it as a context to all our resolvers. Create a context.ts
file under the src
folder and paste the following -
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export type GraphQLContext = {
prisma: PrismaClient;
};
export function createContext() {
return { prisma };
}
Inside the src/index.ts
file pass the prisma client as a context argument to all our resolvers, so that we can access it in our resolvers for querying the database -
import { createServer } from "http";
import { createYoga } from "graphql-yoga";
import { schema } from "./schema";
import { createContext } from "./context";
function main() {
const yoga = createYoga({
schema,
context: createContext(),
});
const server = createServer(yoga);
server.listen(4000, () => {
console.log("Server started on Port no. 4000");
});
}
main();
Finally under the src/schema.ts
file lets implement our simple Todos CRUD GraphQL server -
import { createSchema } from "graphql-yoga";
import { Todos } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { GraphQLError } from "graphql";
import { GraphQLContext } from "./context";
const typeDefinitions = `
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 Comment {
id: ID!
body: String!
}
type Query {
todos: [Todo]!
comment(id: ID!): Comment!
}
input TodoInput {
task: String!
description: String!
status: TaskStatus!
tags: [String]
}
input CommentInput {
todoId: ID!
body: 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!
postCommentOnTodo(input: CommentInput): Comment!
}
`;
const resolvers = {
Query: {
todos(parent: unknown, args: {}, context: GraphQLContext) {
return context.prisma.todos.findMany();
},
comment(parent: unknown, args: { id: string }, context: GraphQLContext) {
return context.prisma.comments.findUnique({
where: {
id: args.id,
},
});
},
},
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);
}
},
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);
}
},
},
};
export const schema = createSchema({
typeDefs: [typeDefinitions],
resolvers: [resolvers],
});
- We created a basic CRUD GraphQL Endpoint, it has 2 queries for reading the
todos & comments
and 4 mutations. - We then resolve the queries (its respective types) & mutations.
- Finally we create our schema using the
createSchema
function and use it in the mainsrc/index.ts
.
Conclusion
We implemented our simple GraphQL server using GraphQL Yoga, but this solution is not scalable, ideally I want my code to be more modular all the features (todos, comments) should be in their respective folders. In the next tutorial we will be using graphql-modules
to create a more maintainable and modular folder structure for our project. Until then PEACE.
Top comments (0)