A comprehensive step by step tutorial on building CRUD Web App using Node, Express, PostgreSQL, Vue 2 and Graphql CRUD Web App. For the client-side (Vue 2) we will use Vue-Apollo module. For the backend side, we will use Node, Express, Sequelize, and PostgreSQL with Express-Graphql module and their dependencies. The scenario for this tutorial is simple as usual, just the CRUD operation which data accessible through GraphQL.

Table of Contents:

The following tools, frameworks, and modules are required for this tutorial:

Node.js (choose recommended version) Vue 2 Express.js GraphQL Express-GraphQL Vue-Apollo Bootstrap-Vue Terminal (Mac/Linux) or Node Command Line (Windows) IDE or Text Editor (We are using Visual Studio Code)

We assume that you have already installed Node.js. Make sure Node.js command line is working (on Windows) or runnable in Linux/OS X terminal.

node -v v10.15.1 npm -v 6.8.0 yarn -v 1.10.1

That the versions that we are uses. Let's continue with the main steps.



Create Express.js Application and Install Required Modules

We will use Express.js as backend server for GraphQL. Express is a web application framework for Node.js, released as free and open-source software under the MIT License. It is designed for building web applications and APIs. It has been called the de facto standard server framework for Node.js. Open your terminal or node command line the go to your projects folder. First, install express-generator using this command.

sudo npm install express-generator -g

Next, create an Express.js app using this command.

express vue-graphql

This will create Express.js project with files and directories.

create : vue-graphql/ create : vue-graphql/public/ create : vue-graphql/public/javascripts/ create : vue-graphql/public/images/ create : vue-graphql/public/stylesheets/ create : vue-graphql/public/stylesheets/style.css create : vue-graphql/routes/ create : vue-graphql/routes/index.js create : vue-graphql/routes/users.js create : vue-graphql/views/ create : vue-graphql/views/error.jade create : vue-graphql/views/index.jade create : vue-graphql/views/layout.jade create : vue-graphql/app.js create : vue-graphql/package.json create : vue-graphql/bin/ create : vue-graphql/bin/www

Next, go to the newly created project folder then install node modules.

cd vue-graphql && npm install

There's no view yet using the latest Express generator. We don't need it because we will create a GraphQL server.



Add and Configure Sequelize.js Module and Dependencies

Sequelize is a promise-based Node.js ORM for Postgres, MySQL, MariaDB, SQLite, and Microsoft SQL Server. It features solid transaction support, relations, eager and lazy loading, read replication and more. Before installing the modules for this project, first, install Sequelize-CLI by type this command.

sudo npm install -g sequelize-cli

To install Sequelize.js module, type this command.

npm install --save sequelize

Then install the module for PostgreSQL.

npm install --save pg pg-hstore

Next, create a new file at the root of the project folder.

touch .sequelizerc

Open and edit that file then add these lines of codes.

const path = require('path'); module.exports = { "config": path.resolve('./config', 'config.json'), "models-path": path.resolve('./models'), "seeders-path": path.resolve('./seeders'), "migrations-path": path.resolve('./migrations') };

That files will tell Sequelize initialization to generate config, models, seeders and migrations files to specific directories. Next, type this command to initialize the Sequelize.

sequelize init

That command will create `config/config.json`, `models/index.js`, `migrations` and `seeders` directories and files. Next, open and edit `config/config.json` then make it like this.

We use the same configuration for all the environment because we are using the same machine, server, and database for this tutorial.

Before run and test connection, make sure you have created a database as described in the above configuration. You can use the `psql` command to create a user and database.

psql postgres --u postgres

Next, type this command for creating a new user with password then give access for creating the database.

postgres-# CREATE ROLE djamware WITH LOGIN PASSWORD '[email protected]@r3'; postgres-# ALTER ROLE djamware CREATEDB;

Quit `psql` then log in again using the new user that previously created.

postgres-# \q psql postgres -U djamware

Enter the password, then you will enter this `psql` console.

psql (9.5.13) Type "help" for help. postgres=>

Type this command to creating a new database.

postgres=> CREATE DATABASE book_store;

Then give that new user privileges to the new database then quit the `psql`.

postgres=> GRANT ALL PRIVILEGES ON DATABASE book_store TO djamware; postgres=> \q



Create or Generate Sequelize Models and Migrations

We will use Sequelize-CLI to generate a new mode and migrationl. Type this command to create a model for 'Book'.

sequelize model:generate --name Book --attributes isbn:string,title:string,author:string,description:string,publishedYear:integer,publisher:string

That commands will generate models and migration files. The content of the model file looks like this.

'use strict'; module.exports = (sequelize, DataTypes) => { const Book = sequelize.define('Book', { isbn: DataTypes.STRING, title: DataTypes.STRING, author: DataTypes.STRING, description: DataTypes.STRING, publishedYear: DataTypes.INTEGER, publisher: DataTypes.STRING }, {}); Book.associate = function(models) { // associations can be defined here }; return Book; };

And the migration file looks like this.

'use strict'; module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.createTable('Books', { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER }, isbn: { type: Sequelize.STRING }, title: { type: Sequelize.STRING }, author: { type: Sequelize.STRING }, description: { type: Sequelize.STRING }, publishedYear: { type: Sequelize.INTEGER }, publisher: { type: Sequelize.STRING }, createdAt: { allowNull: false, type: Sequelize.DATE }, updatedAt: { allowNull: false, type: Sequelize.DATE } }); }, down: (queryInterface, Sequelize) => { return queryInterface.dropTable('Books'); } };

Finally, for migrations, there's nothing to change and they all ready to generate the table to the PostgreSQL Database. Type this command to generate the table to the database.

sequelize db:migrate



Install GraphQL Modules and Dependencies

Now, the GraphQL time. Type this command to install GraphQL modules and it's dependencies.

npm install express express-graphql graphql graphql-date cors --save

Next, open and edit `app.js` then declare all of those modules and dependencies.

var graphqlHTTP = require('express-graphql'); var schema = require('./graphql/bookSchemas'); var cors = require("cors");

The schema is not created yet, we will create it in the next steps. Next, add these lines of codes for configuring GraphQL that can use over HTTP.

app.use('*', cors()); app.use('/graphql', cors(), graphqlHTTP({ schema: schema, rootValue: global, graphiql: true, }));

That's configuration are enabled CORS and the GraphiQL. GraphiQL is the user interface for testing GraphQL query.



Create GraphQL Schemas for the Book

Create a folder at the server folder for hold GraphQL Schema files then create a Javascript file for the schema.

mkdir graphql touch graphql/bookSchemas.js

Next, open and edit `server/graphql/bookSchemas.js` then declares all required modules and models.

var GraphQLSchema = require('graphql').GraphQLSchema; var GraphQLObjectType = require('graphql').GraphQLObjectType; var GraphQLList = require('graphql').GraphQLList; var GraphQLObjectType = require('graphql').GraphQLObjectType; var GraphQLNonNull = require('graphql').GraphQLNonNull; var GraphQLID = require('graphql').GraphQLID; var GraphQLString = require('graphql').GraphQLString; var GraphQLInt = require('graphql').GraphQLInt; var GraphQLDate = require('graphql-date'); var BookModel = require('../models').Book;

Create a GraphQL Object Type for Book models.

var bookType = new GraphQLObjectType({ name: "book", fields: function() { return { id: { type: GraphQLInt }, isbn: { type: GraphQLString }, title: { type: GraphQLString }, author: { type: GraphQLString }, description: { type: GraphQLString }, publishedYear: { type: GraphQLInt }, publisher: { type: GraphQLString }, createdAt: { type: GraphQLDate }, updatedAt: { type: GraphQLDate } }; } });

Next, create a GraphQL query type that calls a list of book and single book by ID.

var queryType = new GraphQLObjectType({ name: 'Query', fields: function () { return { books: { type: new GraphQLList(bookType), resolve: function () { const books = BookModel.findAll({ order: [ ['createdAt', 'DESC'] ], }) if (!books) { throw new Error('Error') } return books } }, book: { type: bookType, args: { id: { name: 'id', type: GraphQLString } }, resolve: function (root, params) { const bookDetails = BookModel.findByPk(params.id).exec() if (!bookDetails) { throw new Error('Error') } return bookDetails } } } } });

Finally, exports this file as GraphQL schema by adding this line at the end of the file.

module.exports = new GraphQLSchema({query: queryType});



Add GraphQL Mutation to the Schema

For completing CRUD (Create, Read, Update, Delete) operation of the GraphQL, we need to add a mutation that contains create, update and delete operations. Open and edit `graphql/bookSchemas.js` then add this mutation as GraphQL Object Type.

var mutation = new GraphQLObjectType({ name: 'Mutation', fields: function () { return { addBook: { type: bookType, args: { isbn: { type: new GraphQLNonNull(GraphQLString) }, title: { type: new GraphQLNonNull(GraphQLString) }, author: { type: new GraphQLNonNull(GraphQLString) }, description: { type: new GraphQLNonNull(GraphQLString) }, publishedYear: { type: new GraphQLNonNull(GraphQLInt) }, publisher: { type: new GraphQLNonNull(GraphQLString) } }, resolve: function (root, params) { const bookModel = new BookModel(params); const newBook = bookModel.save(); if (!newBook) { throw new Error('Error'); } return newBook } }, updateBook: { type: bookType, args: { id: { name: 'id', type: new GraphQLNonNull(GraphQLInt) }, isbn: { type: new GraphQLNonNull(GraphQLString) }, title: { type: new GraphQLNonNull(GraphQLString) }, author: { type: new GraphQLNonNull(GraphQLString) }, description: { type: new GraphQLNonNull(GraphQLString) }, publishedYear: { type: new GraphQLNonNull(GraphQLInt) }, publisher: { type: new GraphQLNonNull(GraphQLString) } }, resolve(root, params) { return BookModel .findByPk(params.id) .then(book => { if (!book) { throw new Error('Not found'); } return book .update({ isbn: params.isbn || book.isbn, title: params.title || book.title, author: params.author || book.author, description: params.description || book.description, publishedYear: params.publishedYear || book.publishedYear, publisher: params.publisher || book.publisher, }) .then(() => { return book; }) .catch((error) => { throw new Error(error); }); }) .catch((error) => { throw new Error(error); }); } }, removeBook: { type: bookType, args: { id: { type: new GraphQLNonNull(GraphQLInt) } }, resolve(root, params) { return BookModel .findByPk(params.id) .then(book => { if (!book) { throw new Error('Not found'); } return book .destroy() .then(() => { return book; }) .catch((error) => { throw new Error(error); }); }) .catch((error) => { throw new Error(error); }); } } } } });

Finally, add this mutation to the GraphQL Schema exports like below.

module.exports = new GraphQLSchema({query: queryType, mutation: mutation});



Test GraphQL using GraphiQL

To test the queries and mutations of CRUD operations, re-run again the Express.js app then open the browser. Go to this address `http://localhost:3000/graphql` to open the GraphiQL User Interface.

To get the list of books, replace all of the text on the left pane with this GraphQL query then click the Play button.

To get a single book by ID, use this GraphQL query.

{ book(id: 1) { id isbn title author description publishedYear publisher updatedAt } }

To add a book, use this GraphQL mutation.

mutation { addBook( isbn: "12345678", title: "Whatever this Book Title", author: "Mr. Bean", description: "The short explanation of this Book", publisher: "Djamware Press", publishedYear: 2019 ) { updatedAt } }

You will the response at the right pane like this.

{ "data": { "addBook": { "updatedAt": "2019-02-26T13:55:39.160Z" } } }

To update a book, use this GraphQL mutation.

mutation { updateBook( id: 1, isbn: "12345678221", title: "The Learning Curve of GraphQL", author: "Didin J.", description: "The short explanation of this Book", publisher: "Djamware Press", publishedYear: 2019 ) { id, updatedAt } }

You will see the response in the right pane like this.

{ "data": { "updateBook": { "id": 1, "updated_date": "2019-02-26T13:58:35.811Z" } } }

To delete a book by ID, use this GraphQL mutation.

mutation { removeBook(id: 1) { id } }

You will see the response in the right pane like this.

{ "data": { "removeBook": { "id": 1 } } }



Create Vue 2 Application

We will use Vue-CLI to create a Vue 2 web app, to install Vue-CLI type this command from the Terminal or Node command line.

sudo npm install -g @vue/cli

or

yarn global add @vue/cli

Next, check the version to make sure that you have the 3.x version of Vue-CLI.

vue --version 3.7.0

Next, create a new Vue.js project by type this command.

vue create client

For now, use the default for every question that shows up in the Terminal. Next, go to the newly created folder.

cd ./client

To make sure that created Vue.js project working, type this command to run the Vue.js application.

npm run serve

or

yarn serve

You will see this page when open `http://localhost:8080/` in the browser.



Install/Configure the Required Modules, Dependencies, and Router

Now, we have to install and configure all of the required modules and dependencies. Type this command to install the modules.

npm install apollo-boost vue-apollo graphql-tag graphql vue-router --save

Next, open and edit `src/main.js` then add these imports.

import ApolloClient from "apollo-boost"; import VueApollo from "vue-apollo";

Add these constant variables then register `VueApollo` in Vue 2 app.

const apolloClient = new ApolloClient({ uri: 'http://localhost:3000/graphql' }); const apolloProvider = new VueApollo({ defaultClient: apolloClient }); Vue.use(VueApollo); new Vue({ apolloProvider, render: h => h(App) }).$mount('#app')

To register or create routes for the whole application navigation, create a router folder and `index.js` file.

mkdir src/router touch src/router/index.js

Open and edit `src/router/index.js` then add these imports.

import VueRouter from 'vue-router' import BookList from '@/components/BookList' import ShowBook from '@/components/ShowBook' import AddBook from '@/components/AddBook' import EditBook from '@/components/EditBook'

Add the router to each component or page.

export default new VueRouter({ routes: [ { path: '/', name: 'BookList', component: BookList }, { path: '/show-book/:id', name: 'ShowBook', component: ShowBook }, { path: '/add-book', name: 'AddBook', component: AddBook }, { path: '/edit-book/:id', name: 'EditBook', component: EditBook } ] })

Add Vue files for above-registered components or pages.

touch src/components/BookList.vue touch src/components/ShowBook.vue touch src/components/AddBook.vue touch src/components/EditBook.vue

Finally, add or register this router file to `src/main.js` by adding these imports.

import VueRouter from 'vue-router' import router from './router'

Register the Vue-Router after `Vue.config`.

Vue.use(VueRouter)

Modify `new Vue` to be like this.

new Vue({ apolloProvider, router, render: h => h(App) }).$mount('#app')



Create Bootstrap-Vue Component to Display List of Books

Before create or show data to the views, we have to add Bootstrap-Vue. Type this command to install the module.

npm i bootstrap-vue

Next, open and edit `src/main.js` then add these imports.

import BootstrapVue from 'bootstrap-vue' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css'

Add this line after `Vue.config`.

Vue.use(BootstrapVue);

Now, open and edit `src/components/BookList.vue` then add this template tags that contain a bootstrap-vue table.

<template> <b-row> <b-col cols="12"> <h2> Book List <b-link href="#/add-Book">(Add Book)</b-link> </h2> <b-table striped hover :items="books" :fields="fields"> <template slot="actions" scope="row"> <b-btn size="sm" @click.stop="details(row.item)">Details</b-btn> </template> </b-table> </b-col> </b-row> </template>

Next, add the `script` tag for hold all Vue 2 codes.

<script></script>

Inside the script tag, add these imports.

import gql from "graphql-tag"; import router from "../router";

Declare the constant variables for GraphQL query.

const GET_BOOKS = gql` { books { id title author } } `;

Add the main Vue 2 export that contains Vue-Apollo calls that filled Vue 2 data.

export default { name: "BookList", apollo: { books: { query: GET_BOOKS, pollInterval: 300 } }, data() { return { fields: { title: { label: "Title", sortable: true, class: "text-left" }, author: { label: "Author", sortable: true, class: "text-left" }, actions: { label: "Action", class: "text-center" } }, books: [] }; }, methods: { details(book) { router.push({ name: "ShowBook", params: { id: book.id } }); } } };

Finally, add the `style` tag for styling the template.

<style> .table { width: 96%; margin: 0 auto; } </style>



Create Bootstrap-Vue Component to Show and Delete Books

To show the book details that contains all book detail, edit and delete buttons, open and edit `src/components/ShowBook.vue` then add these template tags that contain a Bootstrap-Vue component for display the details.

<template> <b-row> <b-col cols="12"> <h2> Book List <b-link href="#/">(Book List)</b-link> </h2> <b-jumbotron> <template slot="header">{{book.title}}</template> <template slot="lead"> ISBN: {{book.isbn}} <br> Author: {{book.author}} <br> Description: {{book.description}} <br> Published Year: {{book.publishedYear}} <br> Publisher: {{book.publisher}} <br> Update At: {{book.updatedAt}} <br> </template> <hr class="my-4"> <b-btn class="edit-btn" variant="success" @click.stop="editBook(book.id)">Edit</b-btn> <b-btn variant="danger" @click.stop="deleteBook(book.id)">Delete</b-btn> </b-jumbotron> </b-col> </b-row> </template>

Next, add the script tag.

<script></script>

Inside the script tag, add these imports.

import gql from "graphql-tag"; import router from "../router";

Declare the constant variables that handle get a single book and delete book queries.

const GET_BOOK = gql` query book($bookId: Int) { book(id: $bookId) { id isbn title author description publishedYear publisher updatedAt } } `; const DELETE_BOOK = gql` mutation removeBook($id: Int!) { removeBook(id: $id) { id } } `;

Inside main Vue export, add all required functions, variables, and Vue-Apollo function.

export default { name: "ShowBook", data() { return { book: '', bookId: parseInt(this.$route.params.id) }; }, apollo: { book: { query: GET_BOOK, pollInterval: 300, variables() { return { bookId: this.bookId }; } } }, methods: { editBook(id) { router.push({ name: "EditBook", params: { id: id } }); }, deleteBook(id) { this.$apollo .mutate({ mutation: DELETE_BOOK, variables: { id: id } }) .then(data => { console.log(data); }) .catch(error => { console.error(error); }); } } };

Finally, add the style tags to give the view some styles.

<style> .jumbotron { padding: 2rem; } .edit-btn { margin-right: 20px; width: 70px; } </style>



Create Bootstrap-Vue Component to Add a New Book

To add a new book, open and edit `src/components/AddBook.vue` then add this Vue 2 template tag that contains a Bootstrap-Vue form.

<template> <b-row> <b-col cols="12"> <h2> Add Book <b-link href="#/">(Book List)</b-link> </h2> <b-jumbotron> <b-form @submit="onSubmit"> <b-form-group id="fieldsetHorizontal" horizontal :label-cols="4" breakpoint="md" label="Enter ISBN" > <b-form-input id="isbn" v-model.trim="book.isbn"></b-form-input> </b-form-group> <b-form-group id="fieldsetHorizontal" horizontal :label-cols="4" breakpoint="md" label="Enter Title" > <b-form-input id="title" v-model.trim="book.title"></b-form-input> </b-form-group> <b-form-group id="fieldsetHorizontal" horizontal :label-cols="4" breakpoint="md" label="Enter Author" > <b-form-input id="author" v-model.trim="book.author"></b-form-input> </b-form-group> <b-form-group id="fieldsetHorizontal" horizontal :label-cols="4" breakpoint="md" label="Enter Description" > <b-form-textarea id="description" v-model="book.description" placeholder="Enter something" :rows="2" :max-rows="6" >{{book.description}}</b-form-textarea> </b-form-group> <b-form-group id="fieldsetHorizontal" horizontal :label-cols="4" breakpoint="md" label="Enter Publisher" > <b-form-input id="publisher" v-model.trim="book.publisher"></b-form-input> </b-form-group> <b-form-group id="fieldsetHorizontal" horizontal :label-cols="4" breakpoint="md" label="Enter Published Year" > <b-form-input type="number" id="publishedYear" v-model.trim="book.publishedYear"></b-form-input> </b-form-group> <b-button type="submit" variant="primary">Save</b-button> </b-form> </b-jumbotron> </b-col> </b-row> </template>

Next, add the script tag.

<script></script>

Inside the script, tag adds Vue 2 codes that contain Vue-Apollo GraphQL mutation to save a new book.

import gql from "graphql-tag"; import router from "../router"; const ADD_BOOK = gql` mutation AddBook( $isbn: String! $title: String! $author: String! $description: String! $publisher: String! $publishedYear: Int! ) { addBook( isbn: $isbn title: $title author: $author description: $description publisher: $publisher publishedYear: $publishedYear ) { id } } `; export default { name: "AddBook", data() { return { book: {} }; }, methods: { onSubmit(evt) { evt.preventDefault(); this.$apollo .mutate({ mutation: ADD_BOOK, variables: { isbn: this.book.isbn, title: this.book.title, author: this.book.author, description: this.book.description, publisher: this.book.publisher, publishedYear: parseInt(this.book.publishedYear) } }) .then(data => { console.log(data); router.push({ name: "BookList" }); }) .catch(error => { console.error(error); }); } } };

Finally, give the view a style by adding the style tag.

<style> .jumbotron { padding: 2rem; } </style>



Create Bootstrap-Vue Component to Edit a Book

To edit a book after getting single book data, open and edit `src/components/EditBook.vue` then add this Vue 2 template that contains a Bootstrap-Vue form.

<template> <b-row> <b-col cols="12"> <h2> Edit Book <router-link :to="{ name: 'ShowBook', params: { id: bookId } }">(Show Book)</router-link> </h2> <b-jumbotron> <b-form @submit="onSubmit"> <b-form-group id="fieldsetHorizontal" horizontal :label-cols="4" breakpoint="md" label="Enter ISBN" > <b-form-input id="isbn" v-model.trim="book.isbn"></b-form-input> </b-form-group> <b-form-group id="fieldsetHorizontal" horizontal :label-cols="4" breakpoint="md" label="Enter Title" > <b-form-input id="title" v-model.trim="book.title"></b-form-input> </b-form-group> <b-form-group id="fieldsetHorizontal" horizontal :label-cols="4" breakpoint="md" label="Enter Author" > <b-form-input id="author" v-model.trim="book.author"></b-form-input> </b-form-group> <b-form-group id="fieldsetHorizontal" horizontal :label-cols="4" breakpoint="md" label="Enter Description" > <b-form-textarea id="description" v-model="book.description" placeholder="Enter something" :rows="2" :max-rows="6" >{{book.description}}</b-form-textarea> </b-form-group> <b-form-group id="fieldsetHorizontal" horizontal :label-cols="4" breakpoint="md" label="Enter Publisher" > <b-form-input id="publisher" v-model.trim="book.publisher"></b-form-input> </b-form-group> <b-form-group id="fieldsetHorizontal" horizontal :label-cols="4" breakpoint="md" label="Enter Published Year" > <b-form-input type="number" id="publishedYear" v-model.trim="book.publishedYear"></b-form-input> </b-form-group> <b-button type="submit" variant="primary">Update</b-button> </b-form> </b-jumbotron> </b-col> </b-row> </template>

Next, add the script tag that contains all required Vue 2 codes with get data and update function.

<script> import gql from "graphql-tag"; import router from "../router"; const GET_BOOK = gql` query book($bookId: Int) { book(id: $bookId) { id isbn title author description publishedYear publisher } } `; const UPDATE_BOOK = gql` mutation updateBook( $id: Int! $isbn: String! $title: String! $author: String! $description: String! $publisher: String! $publishedYear: Int! ) { updateBook( id: $id isbn: $isbn title: $title author: $author description: $description publisher: $publisher publishedYear: $publishedYear ) { updatedAt } } `; export default { name: "EditBook", data() { return { bookId: this.$route.params.id, book: {} }; }, apollo: { book: { query: GET_BOOK, variables() { return { bookId: this.bookId }; } } }, methods: { onSubmit(evt) { evt.preventDefault(); this.$apollo .mutate({ mutation: UPDATE_BOOK, variables: { id: parseInt(this.book.id), isbn: this.book.isbn, title: this.book.title, author: this.book.author, description: this.book.description, publisher: this.book.publisher, publishedYear: parseInt(this.book.publishedYear) } }) .then(data => { console.log(data); router.push({ name: "ShowBook", params: { id: this.$route.params.id } }); }) .catch(error => { console.error(error); }); } } }; </script>

Finally, give the view some style by adding the style tag.

<style> .jumbotron { padding: 2rem; } </style>



Run and Test GraphQL CRUD from the Vue 2 Application

We assume the PostgreSQL server already running, so you just can run Node/Express.js application and Vue 2 app in the separate terminal tabs.

nodemon cd client npm run serve

Next, open the browser then go to this address `localhost:8080` and you should see these pages.







That it's, the Node, Express, PostgreSQL, Vue 2 and Graphql CRUD Web App. You can find the full source code on our GitHub.

That just the basic. If you need more deep learning about Node.js, Express.js, PostgreSQL, Vue.js and GraphQL or related you can take the following cheap course:

Thanks!