GraphQL with MEVN Stack Tutorial
GraphQL MEVN Stack
- MongoDB
- ExpressJS
- VueJS
- 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 };
Hi, I am getting some error on Usage of graphqllist type: Can you please help me with it,
ReplyDeleteC:\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...