Breaking

GraphQL with MEVN Stack Tutorial

Get all code from here

GraphQL MEVN Stack

  1. MongoDB
  2. ExpressJS
  3. VueJS
  4. NodeJS



Here we are going to learn graphql with MEVN stack. Create a root folder and name it anything. inside that root folder create two folders with names of the server and client. the server is for nodejs backend and the client is for Vue js front end.



server

In server folder open terminal or command prompt and initialize nodejs project and install packages that include inside dependencies in package.json in below.



server/package.json

Some description

{
  "name": "mevn-stack-graphql",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "backend": "nodemon index.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "concurrently": "^6.2.1",
    "dotenv": "^10.0.0",
    "nodemon": "^2.0.12"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "express-graphql": "^0.12.0",
    "graphql": "^15.5.3",
    "mongoose": "^6.0.5"
  }
}


server/index.js

Setup server, connect to MongoDB, create GraphQL main request.

require('dotenv').config({ path: "./config/.env" });
const express = require('express');
const mongoose = require('mongoose');
const { graphqlHTTP } = require('express-graphql');
const graphql= require('graphql');
const cors = require('cors');
const GraphQLSchema = graphql.GraphQLSchema;

const RootQuery = require('./graphql/schema/index');
const RootMutation = require('./graphql/resolvers/index');


const app = express();

mongoose.connect(process.env.MONGO_URI, 
{ useUnifiedTopology: true, useNewUrlParser: true }, 
() => console.log('Mongodb is connected'));

app.use(cors());





app.use('/graphql', graphqlHTTP({
    schema: new GraphQLSchema({query: RootQuery, mutation: RootMutation}),
    graphiql: true
}));


const PORT = process.env.PORT || 4000;
app.listen(PORT, () => console.log('Server is running on: ' + PORT));


server/models/Club.js

Create a mongoose schema with name and league field.

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const clubSchema = new Schema({
    name: {
        type: String,
        required: true
    },
    league: {
        type: String,
        required: true
    },
});
module.exports = mongoose.model('club', clubSchema);


server/graphql/types.js

Declare all types we need for our project. In this project, we have only club type.

const {GraphQLString, GraphQLID, GraphQLObjectType} = require('graphql');


const ClubType = new GraphQLObjectType({
    name: "Club",
    fields: ()=>({
        id: {type: GraphQLID},
        name: {type: GraphQLString},
        league: {type: GraphQLString}
    })
});







module.exports = {ClubType};


server/shcema/index.js

Don't confuse with index.js from the root folder and resolvers folder. You can name the folder or file anything you want. Here we will create all of our queries.

const { ClubType } = require('../types');
const { GraphQLObjectType, GraphQLList, GraphQLID} = require('graphql');
const Club = require('../../models/Club');




const clubs = {
    name: "Clubs",
    type: GraphQLList(ClubType),
    resolve: async (parent, args) => {
        const clubs = await Club.find();
        return clubs;
    }
};



const singleClub = {
    name: "singleClub",
    type: ClubType,
    args: {id: {type: GraphQLID}},
    resolve: async (parent, args) => {
        const club = await Club.findById(args.id);
        // console.log(clubs);
        return club;
    }
};


const RootQuery = new GraphQLObjectType({
    name: "RootQuery",
    fields: {
        clubs,
        singleClub
    }
});






module.exports = RootQuery;



/*
const { GraphQLObjectType } = require('graphql');
const { ClubType } = require('../types');




const RootQuery = new GraphQLObjectType({
    name: "RootQuery",
    fields: {
        clubs: {
            name: "Clubs",
            type: ClubType,
            resolve: (parent, args) => {
                return {
                    club: "Some club",
                    league: "Some league"
                };
            }
        }
    }
});


module.exports = RootQuery;
*/


server/resolvers/index.js

Query means only fetching data on the other hand mutation refers to any kind of interaction with server and database. Here is the resolvers folder create index.js declare all mutation.

const { GraphQLObjectType, GraphQLString, GraphQLID } = require('graphql');
const { ClubType } = require('../types');
const Club = require('../../models/Club');


const addClub = {
    name: "addClub",
    type: ClubType,
    args: { name: { type: GraphQLString }, league: { type: GraphQLString } },
    resolve: async (parent, args) => {
        console.log(args.name, args.league);
        const club = new Club({ name: args.name, league: args.league });
        const new_club = await club.save();
        return new_club;
    }
}


const deleteClub = {
    name: "deleteClub",
    type: ClubType,
    args: { id: { type: GraphQLID } },
    resolve: async (parent, args) => {
        const deleted_club = await Club.findByIdAndDelete(args.id);
        return deleted_club;
    }
}


const updateClub = {
    name: "updatelub",
    type: ClubType,
    args: { 
        id: { type: GraphQLID },    
        name: { type: GraphQLString }, 
        league: { type: GraphQLString } 
    },
    resolve: async (parent, args) => {
        const update_club = await Club.findByIdAndUpdate(args.id, {
            name: args.name,
            league: args.league
        });
        return update_club;
    }
}




const RootMutation = new GraphQLObjectType({
    name: "RootMutation",
    fields: {
        addClub,
        deleteClub,
        updateClub
    }
});




module.exports = RootMutation;


client

Our server part is done. Now work with Vue js front-end. Create a client folder and create all front-end files here. At first, create a boilerplate Vue app using Vue CLI.



client/package.json

Some description

{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build"
  },
  "dependencies": {
    "@tailwindcss/postcss7-compat": "^2.0.2",
    "autoprefixer": "^9",
    "core-js": "^3.6.5",
    "postcss": "^7",
    "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2",
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "vue-cli-plugin-tailwind": "~2.0.6"
  }
}


client/src/main.js

run a command from the client folder - vue add tailwind to add tailwind CSS to our project so we don't need to style. This is a great CSS framework to style ourswebpage.

import './assets/tailwind.css';
import { createApp } from 'vue';
import App from './App.vue';
import './assets/tailwind.css';

createApp(App).mount('#app');


client/src/app.vue

Some description

<template>
  <Club />
</template>

<script>
import Club from "./components/Club.vue";

export default {
  name: "App",
  components: {
    Club,
  },
};
</script>

<style>
#app {
  /* font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px; */
}
</style>


client/src/components/Club.vue

Some description

<template>
  <div class="Club">
    <div class="container mx-auto mt-5 bg-gray-700 text-gray-300">
      <h2 class="text-2xl p-3">UCL Clubs</h2>
      <div class="forms flex flex-col w-full items-center">
        <div class="box flex flex-col w-1/2 mb-3">
          <label for="name" class="text-sm">Club Name</label>
          <input
            type="text"
            id="name"
            v-model="state.club.name"
            class="bg-gray-800 py-1 rounded focus outline-none px-2"
          />
        </div>
        <div class="box flex flex-col w-1/2 mb-3">
          <label for="league" class="text-sm">League</label>
          <input
            type="text"
            id="league"
            v-model="state.club.league"
            class="bg-gray-800 py-1 rounded focus outline-none px-2"
          />
        </div>
        <button
          v-on:click="submitItemHandler"
          class="bg-gray-200 px-5 py-2 mb-5 rounded text-gray-900"
        >
          Submit
        </button>
      </div>
    </div>
    <br />
    <div class="all-clubs mt-4 container mx-auto">
      <ul
        class="list space-y-2 flex inline-flex flex-wrap justify-between w-full"
      >
        <li
          class="
            text-gray-300
            bg-gray-800
            list-item
            shadow
            hover:bg-gray-700
            transition-all transition-colors
          "
          v-for="(item, i) in state.clubList"
          v-bind:key="i"
        >
          <div
            class="
              inside-list
              flex
              justify-between
              flex-row
              border-b border-solid border-indigo-900
              mb-2
              p-2
            "
          >
            <p class="w-1/2">{{ item.name }}</p>
            <p class="w-1/2">{{ item.league }}</p>
          </div>
          <div class="buttons p-2">
            <button
              class="bg-red-600 py-2 px-2 rounded mr-2"
              v-on:click="(e) => deleteItemHandler(e, item.id)"
            >
              Delete
            </button>
            <button
              class="bg-blue-600 py-2 px-2 rounded mr-2"
              v-on:click="(e) => updateItemHandler(e, item.id)"
            >
              Update
            </button>
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>


<script>
import { reactive, onMounted } from "vue";
import { getClubs } from "../graphql/queries";
import { addClub, deleteClub, updateClub } from "../graphql/mutations";
export default {
  setup(props) {
    const state = reactive({
      club: {
        id: null,
        name: null,
        league: null,
      },
      clubList: [],
      update: false,
      isLoading: false,
    });

    onMounted(async () => {
      const allClubs = await getClubs(state);
      console.log(allClubs);
      state.clubList = allClubs;
    });

    const submitItemHandler = async (e) => {
      e.preventDefault();
      // console.log(state.club.name);
      // ADDING NEW CLUB

      if (state.update === true) {
        // UPDATE EXISTING CLUB
        const updatedClub = await updateClub(state, state.club.id);
      } else {
        const newClub = await addClub(state);
      }

      // FETCHING ONCE AGAIN TO UPDATE CLIB LIST
      const allClubs = await getClubs(state);
      // console.log(allClubs);
      state.clubList = allClubs;
      state.club = { id: null, name: null, league: null };
      state.update = false;
    };

    const deleteItemHandler = async (e, id) => {
      e.preventDefault();
      // console.log(itemID);
      const deletedClubs = await deleteClub(state, id);
      // FETCHING ONCE AGAIN TO UPDATE CLIB LIST
      const allClubs = await getClubs(state);
      // console.log(allClubs);
      state.clubList = allClubs;
    };

    const updateItemHandler = async (e, id) => {
      e.preventDefault();
      const selectedClub = state.clubList.filter(
        (club, i) => club.id === id
      )[0];
      state.club.name = selectedClub.name;
      state.club.league = selectedClub.league;
      state.club.id = selectedClub.id;
      // console.log(state);
      state.update = true;
    };

    return {
      updateItemHandler,
      submitItemHandler,
      deleteItemHandler,
      state,
    };
  },
};
</script>


<style scoped>
.list-item {
  min-width: 20em;
  min-height: 6em;
  padding: 0;
  margin: 0;
}
</style>


client/src/config/keys.js

All the globals variables we can declare here so we can change easily.

export const HOSTNAME = 'http://localhost:8000/graphql';


client/src/graphql/queries.js

Make all the queries by using fetch API. You can also you apollo which is very popular.

import { HOSTNAME } from '../config/keys';
const getClubs = async (state) => {

    state.isLoading = true;

    const options = {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            query: `{
                clubs{
                        name    
                        id
                        league
                    }
                }`
        })
    };
    const response = await fetch(HOSTNAME, options);
    // console.log(response);
    const text = await response.text();
    const jsonRes = JSON.parse(text);
    // console.log(jsonRes.data.clubs);
    state.isLoading = false;
    return jsonRes.data.clubs;
}



export { getClubs };


client/src/graphql/mutations.js

Make all mutations

import { HOSTNAME } from '../config/keys';

const addClub = async (state) => {
    state.isLoading = true;
    console.log(`name: ${state.club.name}, league: ${state.club.league}`);

    const options = {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            query: `
                    mutation{
                        addClub(name: "${state.club.name}", league: "${state.club.league}"){
                            name 
                            league
                            id
                        }
                    }
                `
        }),
    };
    const response = await fetch(HOSTNAME, options);
    // console.log(response);
    const text = await response.text();
    const jsonRes = JSON.parse(text);
    console.log(jsonRes);
    state.isLoading = false;
    // return jsonRes.data.clubs;
}


const deleteClub = async (state, id) => {
    state.isLoading = true;


    const options = {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            query: `
                mutation{
                    deleteClub(id:"${id}"){
                        name
                    }
                }
                `
        }),
    };
    const response = await fetch(HOSTNAME, options);
    console.log("Delete item - ",response);
    state.isLoading = false;
    // return jsonRes.data.clubs;
}



const updateClub = async (state, id) => {
    state.isLoading = true;

    const options = {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            query: `
                    mutation{
                        updateClub(id:"${id}", name:"${state.club.name}", league:"${state.club.league}"){
                            id
                            name
                            league
                          }
                    }
                `
        }),
    };
    const response = await fetch(HOSTNAME, options);
    console.log("Update clubs ",response);
    state.isLoading = false;
    // return jsonRes.data.clubs;
}





export { addClub, deleteClub, updateClub };



Youtube tutorial playlist





1 comment:

  1. Hi, I am getting some error on Usage of graphqllist type: Can you please help me with it,
    C:\Users\pavan\Documents\MEVN\server\node_modules\graphql\type\definition.js:97
    throw new Error("Expected ".concat((0, _inspect.default)(type), " to be a GraphQL type."));
    ^

    Error: Expected undefined to be a GraphQL type.
    at assertType (C:\Users\pavan\Documents\MEVN\server\node_modules\graphql\type\definition.js:97:11)
    at new GraphQLList (C:\Users\pavan\Documents\MEVN\server\node_modules\graphql\type\definition.js:323:19)
    at GraphQLList (C:\Users\pavan\Documents\MEVN\server\node_modules\graphql\type\definition.js:325:12)
    at Object. (C:\Users\pavan\Documents\MEVN\server\graphql\schema\index.js:10:11)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    [nodemon] app crashed - waiting for file changes before starting...

    ReplyDelete

Powered by Blogger.