DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Getting Started, GraphQL & Node.js in 2023 - GraphQL Yoga

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 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.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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. */
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
},
Enter fullscreen mode Exit fullscreen mode

From the root of the project create a .gitignore file -

node_modules
.env
dist
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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

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],
});
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Save the file, navigate to the GraphiQL and verify the following todos query -

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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],
});
Enter fullscreen mode Exit fullscreen mode
  • 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.

Top comments (0)