DEV Community

Cover image for Build a GraphQL Todolist server on Prisma/PostgreSQL
pcreem
pcreem

Posted on • Updated on

Build a GraphQL Todolist server on Prisma/PostgreSQL

Github and Demo

Brief intro about GraphQL and Prisma

GraphQL is developed by Facebook in 2015. On the Client side, It makes nest data fetching easier by JSON like interface (as the image above), rather than multiple URLs or ORM/database request. On the Server side, you can update the data model by add or delete row at the aging field.

Prisma is an alternative ORM and SQL query builder.

The following will show how to build a GraphQL backend from scratch.

prerequisites: 

A. setup database: 

mkdir todo
mkdir todo/backend
cd todo/backend
npm init -y
npm install @prisma/cli - save-dev
npx prisma init

database model:

code ./prisma/schema.prisma

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Post {
  id        Int      @default(autoincrement()) @id
  createdAt DateTime @default(now())
  title     String
  content   String? //question mark means opational
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}

model User {
  id       Int     @default(autoincrement()) @id
  email    String  @unique
  name     String?
  password String
  posts    Post[]
}

code ./prisma/.env

DATABASE_URL=postgresql://USER:PASSWORD@HOST:PORT/DATABASE

Prisma Migrate:

npx prisma migrate save --name init --experimental
npx prisma migrate up --experimental
npx prisma generate

B. Build server

npm i graphql-yoga @prisma/client
code index.js

const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
const { GraphQLServer } = require('graphql-yoga')

const Query = require('./resolvers/query.js')
const Mutation = require('./resolvers/mutation.js')
const User = require('./resolvers/user.js')
const Post = require('./resolvers/post.js')

const resolvers = {
  Query,
  Mutation,
  User,
  Post
}

const server = new GraphQLServer({
  typeDefs: './schema.graphql',
  resolvers,
  context: request => {
    return {
      ...request,
      prisma,
    }
  },
})

const PORT = process.env.PORT || 4000
server.start(PORT, () => console.log(`Server is running on http://localhost:4000`))
  1. The typeDefs connect to the schema wrote at the t section. It defined the database path, data type, and the relation between them.
  2. The resolvers defined how to deal with data in each relevant script. For example, Query responsible for fetching data, Mutation for CRUD/ Signup/Login function, we will deal with it later. The rest two (User and Post in resolvers field) define data relations. 
  3. The context contains custom data being passed through your resolver chain.

C. Build typeDefs, resolvers, and user identification

  • define typeDefs code schema.graphql
type Query {
  info: [Post!]!
}

type Mutation {
  upsertPost (postId:ID!, title: String!, content: String): Post!
  deletePost (postId:ID!): Post

  signup(email: String!, password: String!, name: String!): AuthPayload
  login(email: String!, password: String!): AuthPayload
}

type Post {
  id: ID!
  title: String!
  content: String
  author:    User
  createdAt: String!
}

type AuthPayload {
  token: String
  user: User
}

type User {
  id: ID!
  name: String
  email: String!
  posts: [Post]
}
  • define resolvers

npm i jsonwebtoken bcryptjs dotenv
mkdir resolvers
code ./resolvers/query.js

const { getUserId } = require('../utils')

function info(parent, args, context, info) {
  const userId = getUserId(context)
  const Posts = context.prisma.post.findMany({
    where: {
      authorId: parseInt(userId)
    }
  })

  return Posts
}


module.exports = {
  info
}

code ./resolvers/mutation.js

const { getUserId } = require('../utils')
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
require('dotenv').config()
const APP_SECRET = process.env.APP_SECRET

function upsertPost(parent, args, context, info) {
  const userId = getUserId(context)
  const upsertPost = context.prisma.post.upsert({
    where: {
      id: parseInt(args.postId)
    },
    update: {
      title: args.title,
      content: args.content,
    },
    create: {
      title: args.title,
      content: args.content,
      author: {
        connect: { id: parseInt(userId) },
      },
    },
  })

  return upsertPost
}


function deletePost(parent, args, context, info) {
  const deletePost = context.prisma.post.delete({

    where: {
      id: parseInt(args.postId),
    },
  })

  return deletePost
}

async function signup(parent, args, context, info) {
  const password = await bcrypt.hash(args.password, 10)
  const user = await context.prisma.user.create({ data: { ...args, password } })
  const token = jwt.sign({ userId: user.id }, APP_SECRET)

  return {
    token,
    user,
  }
}

async function login(parent, args, context, info) {
  const user = await context.prisma.user.findOne({ where: { email: args.email } })
  if (!user) {
    throw new Error('No such user found')
  }
  const valid = await bcrypt.compare(args.password, user.password)
  if (!valid) {
    throw new Error('Invalid password')
  }
  const token = jwt.sign({ userId: user.id }, APP_SECRET)

  return {
    token,
    user,
  }
}

module.exports = {
  upsertPost,
  deletePost,

  signup,
  login,
}

code ./resolvers/user.js

function posts(parent, args, context) {
  return context.prisma.user.findOne({ where: { id: parent.id } }).posts()
}

module.exports = {
  posts,
}

code ./resolvers/post.js

function author(parent, args, context) {
  return context.prisma.post.findOne({ where: { id: parent.id } }).author()
}

module.exports = {
  author,
}

The four scrips function have short explained on the above second item (The resolvers).

  • Build a helper to handle user identification code utils.js
const jwt = require('jsonwebtoken')
require('dotenv').config()
const APP_SECRET = process.env.APP_SECRET

function getUserId(context) {
  const Authorization = context.request.get('Authorization')
  if (Authorization) {
    const token = Authorization.replace('Bearer ', '')
    const { userId } = jwt.verify(token, APP_SECRET)
    return userId
  }

  throw new Error('Not authenticated')
}

module.exports = {
  getUserId,
}

code .env

APP_SECRET = Your_APP_SECRET

D. Start GraphQL server

node index.js

you could try the following command on GraphQL left column

  • signup
mutation {
  signup(
    name: "__yourname__"
    email: "__your@email.com__"
    password: "__yourpassword__"
  ) {
    token
    user {
      id
    }
  }
}
  • login
mutation {
  login(
    email: "__your@email.com__"
    password: "__yourpassword__"
  ) {
    token
    user {
      id
      name
      posts{
        id
        title
      }
    }
  }
}
  • add todo

a. copy token to Header(bottom left)

{ "Authorization": "Bearer __Token__" }

b. command

mutation {
  upsertPost(
    postId:"0"
    title: "www.graphqlconf.org"
  ) {
    id
    titile
  }
}
  • delete todo
mutation {
  deletePost(
    postId:"__Id__" //enter todo id
  ) {
    id
  }
}

Top comments (0)