Getting Started, GraphQL & Node.js in 2023 - GraphQL Yoga
Tags: node, graphql, typescript, prisma
Published: 2023-05-31 10:01:01

### 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] (https://hasura.io/),  [Grafbase](https://grafbase.com/) and [WunderGraph](https://wundergraph.com/) 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` following `schema-first` approach.
- In the second tutorial we will modularize our `schema-first` codebase using `graphql-modules`.
- In the third tutorial we will be using `graphql-pothos` to create a `code-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](https://github.com/yaldram/todos-graphql-server).

### Step One: Bootstrap the project
First create a new folder and from the terminal run `npm init` - 
```bash
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 : -
```bash
yarn add -D typescript @types/node nodemon
```

From the root of our project create a new file `tsconfig.json` - 
```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](https://dev.to/yaldram/setup-express-with-typescript-base-project-setup-4m58) I used `ts-node`, this time we will use `esbuild` for faster build times. From your terminal - 
```bash
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` - 
```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 - 
```json
"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 - 
```bash
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 - 
```ts
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 - 

![hello-query](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fm8374gl4wqx53o3xtt8.png)

Instead of writing the schema in the main `index.ts` file, we will create a new file `src/schema.ts` - 
```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 - 
```ts
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 - 

![todos-query](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ql2v5rg7rggrw3zfo231.png)

### 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](https://neon.tech/) service for my database. From your terminal run - 
```bash
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](https://neon.tech/).

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 - 
```ts
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 - 
```ts
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 - 
```ts
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 main `src/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.