How to Build a GraphQL API in Express with File Upload

How to Build a GraphQL API in Express with File Upload

How to Build a GraphQL API in Express with File Upload. GraphQL is a query language made by Facebook for sending requests over the internet. It uses its own query but still sends data over HTTP. It uses one endpoint only for sending data.

GraphQL is a query language made by Facebook for sending requests over the internet. It uses its own query but still sends data over HTTP. It uses one endpoint only for sending data.

The benefits of using GraphQL include being able to specify data types for the data fields you are sending and being able to specify the types of data fields that are returned.

The syntax is easy to understand, and it is simple. The data are still returned in JSON for easy access and manipulation. This is why GraphQL has been gaining traction in recent years.

GraphQL requests are still HTTP requests. However, you are always sending and getting data over one endpoint. Usually, this is the graphql endpoint. All requests are POST requests, no matter if you are getting, manipulating, or deleting data.

To distinguish between getting and manipulating data, GraphQL requests can be classified as queries and mutations. Below is one example of a GraphQL request:

{
  getPhotos(page: 1) {
    photos {
      id
      fileLocation
      description
      tags
    }
    page
    totalPhotos
  }
}

With this request, we are instructing the server to call the getPhotos resolver, which is a function that will return the data, with the argument page set to 1. We also want to get back the id, fileLocation, description, and tags field of the photos array, as well as the pageand totalPhotos fields.

GraphQL APIs can use any database system since it only changes the API layer. The logic underneath is still the same as any REST API.

Node.js with Express has great support for making GraphQL APIs. We can use the express-graphql library to build our GraphQL API. It is a middleware that allows you get GraphQL functionality in your Express back-end app.

Building the Back End

In this story, we will build an image gallery app with a GraphQL API that accepts file uploads. We’ll add some text data using Express and include an Angular front end that uses Material Design with Angular Material.

We start with the back end part of our image-gallery app. To start building the app, we create a new project folder with a backend folder inside to store the back-end files.

Then we go into the folder and run npx express-generator to generate the files for the app.

After that, we need to install some files to let us use the latest features of JavaScript in our app.

First, we install packages for the skeleton app that we generated by running npm i.

Then we run npm i @babel/cli @babel/core @babel/node @babel/preset-env to install the latest Babel packages to get the latest JavaScript features into our app.

Next, we need to install nodemon globally to let us automatically restart our app during development as our code file changes. Make a file called .babelrc at the root level of the back-end app’s project folder, and add the following:

{
    "presets": [
        "@babel/preset-env"
    ],
}

Then in the scripts section of package.json, we put:

"babel-node": "babel-node",
"start": "nodemon --exec npm run babel-node -- ./bin/www"

This allows us to run our app with the latest JavaScript features available. If you get errors, uninstall previous versions of the Babel CLI and Babel Core packages, and try the steps above again. ./bin/www is the entry for the back-end app.

Next, we need to use Sequelize CLI to add the initial ORM code to our back-end app. To do this, we run npx sequelize-cli init to add the ORM code into our app. You should have config/config.json and a models folder created. Then we run npm i sequelize to install the Sequelize library.

Then we can make our model by running npx sequelize-cli model:generate --name Photo --attributes fileLocation:string,description:string,tags:string

This will create the photo model and a photos table in our database when we run the migration that was created with that command.

Next, we rename config.json to config.js and install the dotenv and Postgres packages by running npm i pg pg-hstore.

Then in config/config.js, we put:

require('dotenv').config();
const dbHost = process.env.DB_HOST;
const dbName = process.env.DB_NAME;
const dbUsername = process.env.DB_USERNAME;
const dbPassword = process.env.DB_PASSWORD;
const dbPort = process.env.DB_PORT || 5432;
module.exports = {
    development: {
        username: dbUsername,
        password: dbPassword,
        database: dbName,
        host: dbHost,
        port: dbPort,
        dialect: 'postgres'
    },
    test: {
        username: dbUsername,
        password: dbPassword,
        database: 'graphql_app_test',
        host: dbHost,
        port: dbPort,
        dialect: 'postgres'
    },
    production: {
        use_env_variable: 'DATABASE_URL',
        username: dbUsername,
        password: dbPassword,
        database: dbName,
        host: dbHost,
        port: dbPort,
        dialect: 'postgres'
    }
};

This lets us get our database credentials and name from the .env file we made in the root of the back-end app’s project folder. We have to make an empty database before running our migration. Create an empty database with the name of your choice. Set the name for the value of the DB_NAME key of the .env file, and do the same with the database password.

Now we have everything to run our migration. We run it by running npx sequelize-cli db:migrate. You should have an empty table with the photos table.

Next, we make thefiles folder and put an empty .gitkeep file in it so we can commit it.

After the database connection is established, we can start building the logic. Since we are building a GraphQL API, we need to install the GraphQL libraries for Express.

To do this, we run npm i cors express-graphql graphql graphql-tools graphql-upload . We need the cors library so that we can communicate with our front-end app, which will be hosted in a different domain. The other ones are GraphQL libraries. graphql-upload will allow us to accept files easily in our GraphQL endpoints. You can just pass a JavaScript file object straight in, and it can be saved to disk after converting it to a read stream.

After installing the libraries, we need to write the logic for our app. We make a folder called graphql in the root folder of our back-end app, which will hold the files with the logic for our app. Next, we make a file called resolvers.js and add the following:

const Op = require('sequelize').Op;
const models = require('../models');
const fs = require('fs');
const storeFS = ({ stream, filename }) => {
    const uploadDir = '../backend/photos';
    const path = `${uploadDir}/${filename}`;
    return new Promise((resolve, reject) =>
        stream
            .on('error', error => {
                if (stream.truncated)
                    // delete the truncated file
                    fs.unlinkSync(path);
                reject(error);
            })
            .pipe(fs.createWriteStream(path))
            .on('error', error => reject(error))
            .on('finish', () => resolve({ path }))
    );
}
export const getPhotos = async (args) => {
    const page = args.page;
    const photos = await models.Photo.findAll({
        offset: (page - 1) * 10,
        limit: 10
    });
    const totalPhotos = await models.Photo.count();
    return {
        photos,
        page,
        totalPhotos
    };
}
export const addPhoto = async (args) => {
    const { description, tags } = args;
    const { filename, mimetype, createReadStream } = await args.file;
    const stream = createReadStream();
    const pathObj = await storeFS({ stream, filename });
    const fileLocation = pathObj.path;
    const photo = await models.Photo.create({
        fileLocation,
        description,
        tags
    })
    return photo;
}
export const editPhoto = async (args) => {
    const { id, description, tags } = args;
    const { filename, mimetype, createReadStream } = await args.file;
    const stream = createReadStream();
    const pathObj = await storeFS({ stream, filename });
    const fileLocation = pathObj.path;
    const photo = await models.Photo.update({
        fileLocation,
        description,
        tags
    }, {
            where: {
                id
            }
        })
    return photo;
}
export const deletePhoto = async (args) => {
    const { id } = args;
    await models.Photo.destroy({
        where: {
            id
        }
    })
    return id;
}
export const searchPhotos = async (args) => {
    const searchQuery = args.searchQuery;
    const photos = await models.Photo.findAll({
        where: {
            [Op.or]: [
                {
                    description: {
                        [Op.like]: `%${searchQuery}%`
                    }
                },
                {
                    tags: {
                        [Op.like]: `%${searchQuery}%`
                    }
                }
            ]
        }
});
    const totalPhotos = await models.Photo.count();
    return {
        photos,
        totalPhotos
    };
}

In the code above, we have the resolvers which the GraphQL requests will ultimately be directed to.

We have resolvers for adding a photo by accepting a file along with its description and tags strings. Edit endpoint is similar except that is also accepts an ID, which is an integer, and allows users to save their photo. The delete resolver takes an ID and lets people delete their photo table entry. Note that all the arguments for the request are in the args parameter.

The file that we upload ends up as promised in the args object. We can get it easily, convert it to a stream, and save it as we did with the storeFS function. We return a promise to easily save the data and save the text data to the database sequentially.

The searchPhotos resolver, take a string for the search query and then does a where…or query in the database with the following object:

where: {
   [Op.or]: [
     {
       description: {
         [Op.like]: `%${searchQuery}%`
       }
     },
     {
      tags: {
          [Op.like]: `%${searchQuery}%`
      }
    }
  ]
}

This searches both the description and the tags column for the search query.

Next, we create a file called schema.js in the graphql folder and add the following:

const { buildSchema } = require('graphql');
export const schema = buildSchema( `
    scalar Upload
    type Photo {
        id: Int,
        fileLocation: String,
        description: String,
        tags: String
    }
    type PhotoData {
        photos: [Photo],
        page: Int,
        totalPhotos: Int
    }
    type Query {
        getPhotos(page: Int): PhotoData,
        searchPhotos(searchQuery: String): PhotoData
    }
    type Mutation {
        addPhoto(file: Upload!, description: String, tags: String): Photo
        editPhoto(id: Int, file: Upload!, description: String, tags: String): Photo
        deletePhoto(id: Int): Int
    }
`);

When we define the types of data for our queries and mutations, note that we also defined a new scalar type called Upload in the file to enable us to take file data with the graphql-upload library.

Query includes all your queries. The code left of the colon is the function signature for your resolvers, and the right side is the data type it returns.

Types Photo and PhotoData are types we defined by adding fields of the scalar types.

Int andString are basic types that are included with the express-graphql package. Anything with an exclamation mark is required.

The buildSchema function builds the schema which we will use with the Express GraphQL middleware.

getPhotos and searchPhotos are the query endpoints addPhoto, editPhoto, and deletePhoto. We may call these endpoints in our requests as we do in the example at the beginning of the story.

Next in app.js, we put the following:

const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const expressGraphql = require('express-graphql');
const cors = require('cors');
const app = express();
import { GraphQLUpload } from 'graphql-upload'
import { schema } from './graphql/schema'
import {
  getPhotos,
  addPhoto,
  editPhoto,
  deletePhoto,
  searchPhotos
} from './graphql/resolvers'
import { graphqlUploadExpress } from 'graphql-upload'
const root = {
  Upload: GraphQLUpload,
  getPhotos,
  addPhoto,
  editPhoto,
  deletePhoto,
  searchPhotos
}
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(cors());
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/photos', express.static(path.join(__dirname, 'photos')));
app.use(
  '/graphql',
  graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }),
  expressGraphql({
    schema,
    rootValue: root,
    graphiql: true
  })
)
// catch 404 and forward to error handler
app.use(function (req, res, next) {
  next(createError(404));
});
// error handler
app.use(function (err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
  res.status(err.status || 500);
  res.render('error');
});
module.exports = app;

We include the CORS middleware for cross-domain communication, and the only endpoint that we have is the graphql endpoint. We have the graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }) in the argument to enable file uploads. Now we have:

const root = {
  Upload: GraphQLUpload,
  getPhotos,
  addPhoto,
  editPhoto,
  deletePhoto,
  searchPhotos
}

and

expressGraphql({
  schema,
  rootValue: root,
  graphiql: true
})

to connect the schema and resolvers together and enable our GraphQL endpoints. graphiql: true enables an interactive sandbox from which we can test our GraphQL requests.

Finally, in bin/www, we have:

#!/usr/bin/env node
require('dotenv').config();
/**
 * Module dependencies.
 */
var app = require('../app');
var debug = require('debug')('backend:server');
var http = require('http');
/**
 * Get port from environment and store in Express.
 */
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
 * Create HTTP server.
 */
var server = http.createServer(app);
/**
 * Listen on provided port, on all network interfaces.
 */
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
 * Normalize a port into a number, string, or false.
 */
function normalizePort(val) {
  var port = parseInt(val, 10);
if (isNaN(port)) {
    // named pipe
    return val;
  }
if (port >= 0) {
    // port number
    return port;
  }
return false;
}
/**
 * Event listener for HTTP server "error" event.
 */
function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }
var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;
// handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}
/**
 * Event listener for HTTP server "listening" event.
 */
function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

Together, these code files will enable us to run our GraphQL API with npm start.

Building the Front End

Next, we build the front-end app. First, install the Angular CLI by running npm i -g @angular/cli.

Then go to the root of the project folder, and run ng new frontend to scaffold the front-end app. Make sure routing and SCSS are selected when asked if you want to include routing and styling options respectively.

Next, we install our libraries: We need a GraphQL client, Angular Material, and a Flux library for storing the state of our app. We install those by running npm i @ngrx/store @angular/cdk @angular/material. This command will install the Flux library and Angular Material respectively.

Next, we run ng add @ngrx/store to run the skeleton code for the NgRx store.

To install Angular Apollo, which is the GraphQL client for Angular, we run ng add apollo-angular. This will add a new module and other code to enable us to use GraphQL in our Angular app.

The front-end app will consist of the page where users can get and search their photos and another page where they can upload new photos and edit or delete existing ones. The page where they can get or search their photos will be the home page. It will have a left side menu for navigation.

Now we are ready to write the code. We first run some commands to create some new code files:

ng g component editPhotoDialog --module app
ng g component homePage --module app
ng g component topBar --module app
ng g component uploadPage --module app
ng g service photo --module app

Note that we have to specify the module we want to add the code to by adding the --module app option so they can be used in our main app module.

In photo.service.ts, which should be created from those commands, we put:

import { Injectable } from '@angular/core';
import { Apollo } from 'apollo-angular';
import gql from 'graphql-tag';
@Injectable({
  providedIn: 'root'
})
export class PhotoService {
  constructor(
    private apollo: Apollo
  ) { }
  addPhoto(file: File, description: string, tags: string) {
    const addPhoto = gql`
      mutation addPhoto(
        $file: Upload!,
        $description: String,
        $tags: String
      ){
        addPhoto(
          file: $file,
          description: $description,
          tags: $tags
        ) {
          id,
          fileLocation,
          description,
          tags
        }
      }
    `;
    return this.apollo.mutate({
      mutation: addPhoto,
      variables: {
        file,
        description,
        tags
      },
      context: {
        useMultipart: true
      }
    })
  }
  editPhoto(id: number, file: File, description: string, tags: string) {
    const editPhoto = gql`
      mutation editPhoto(
        $id: Int!,
        $file: Upload!,
        $description: String,
        $tags: String
      ){
        editPhoto(
          id: $id,
          file: $file,
          description: $description,
          tags: $tags
        ) {
          id,
          fileLocation,
          description,
          tags
        }
      }
    `;
    return this.apollo.mutate({
      mutation: editPhoto,
      variables: {
        id,
        file,
        description,
        tags
      },
      context: {
        useMultipart: true
      }
    })
  }
  getPhotos(page: number = 1) {
    const getPhotos = gql`
      query getPhotos(
        $page: Int,
      ){
        getPhotos(
          page: $page
        ) {
          photos {
            id,
            fileLocation,
            description,
            tags
          },
          page,
          totalPhotos
        }
      }
    `;
    return this.apollo.mutate({
      mutation: getPhotos,
      variables: {
        page,
      }
    })
  }
deletePhoto(id: number) {
    const deletePhoto = gql`
      mutation deletePhoto(
        $id: Int,
      ){
        deletePhoto(
          id: $id
        )
      }
    `;
    return this.apollo.mutate({
      mutation: deletePhoto,
      variables: {
        id,
      }
    })
  }
  searchPhotos(searchQuery: string) {
    const getPhotos = gql`
      query searchPhotos(
        $searchQuery: String,
      ){
        searchPhotos(
          searchQuery: $searchQuery
        ) {
          photos {
            id,
            fileLocation,
            description,
            tags
          },
          page,
          totalPhotos
        }
      }
    `;
    return this.apollo.mutate({
      mutation: getPhotos,
      variables: {
        searchQuery,
      }
    })
  }
}

This makes use of the Apollo client we just added. The gql before the query string is a tag that is parsed by the gql tag into a query that Apollo can use.

The syntax is very close to the request example above, except that you pass in variables instead of numbers or strings. Files are also passed in as variables directly. The useMultipart: true option in the context object lets us upload files with Angular Apollo.

Then in edit-photo-dialog.component.ts, we put:

import { Component, OnInit, Inject, ViewChild } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { PhotoService } from '../photo.service';
import { environment } from 'src/environments/environment';
import { Store, select } from '@ngrx/store';
import { SET_PHOTOS } from '../reducers/photos-reducer';
import { NgForm } from '@angular/forms';
@Component({
  selector: 'app-edit-photo-dialog',
  templateUrl: './edit-photo-dialog.component.html',
  styleUrls: ['./edit-photo-dialog.component.scss']
})
export class EditPhotoDialogComponent implements OnInit {
  @ViewChild('photoUpload', null) photoUpload: any;
  photoArrayData: any[] = [];
  constructor(
    public dialogRef: MatDialogRef<EditPhotoDialogComponent>,
    @Inject(MAT_DIALOG_DATA) public photoData: any,
    private photoService: PhotoService,
    private store: Store<any>
  ) {
    store.pipe(select('photos'))
      .subscribe(photos => {
        this.photoArrayData = photos;
      })
  }
  ngOnInit() {
  }
  clickUpload() {
    this.photoUpload.nativeElement.click();
  }
  handleFileInput(files) {
    console.log(files);
    this.photoData.file = files[0];
  }
  save(uploadForm: NgForm) {
    if (uploadForm.invalid || !this.photoData.file) {
      return;
    }
    const {
      id,
      file,
      description,
      tags
    } = this.photoData;
    this.photoService.editPhoto(id, file, description, tags)
      .subscribe(es => {
        this.getPhotos();
      })
  }
  getPhotos() {
      this.photoService.getPhotos()
      .subscribe(res => {
        const photoArrayData = (res as any).data.getPhotos.photos.map(p => {
          const { id, description, tags } = p;
          const pathParts = p.fileLocation.split('/');
          const photoPath = pathParts[pathParts.length - 1];
          return {
            id,
            description,
            tags,
            photoUrl: `${environment.photosUrl}/${photoPath}`
          }
        });
        this.store.dispatch({ type: SET_PHOTOS, payload: photoArrayData });
        this.dialogRef.close()
      })
  }
}

This is code for the dialog box we create when users click edit on a row of photos. The photo data is passed in from the homepage, and they can be edited here. Once the user clicks the Save button, the save function will be called and if that is success, it will call the getPhotos function to get the latest photos and store it in the store.

Next in edit-photo-dialog.component.html , we put:

<h2>Edit Photo</h2>
<form #photoForm='ngForm' (ngSubmit)='save(photoForm)'>
    <div>
        <input type="file" id="file" (change)="handleFileInput($event.target.files)" #photoUpload>
        <button mat-raised-button (click)='clickUpload()' type='button'>
            Upload Photo
        </button>
        {{photoData?.file?.name}}
    </div>
    <mat-form-field>
        <input matInput placeholder="Description" required #description='ngModel' name='description'
            #description='ngModel' [(ngModel)]='photoData.description'>
        <mat-error *ngIf="description.invalid && (description.dirty || description.touched)">
            <div *ngIf="description.errors.required">
                Description is required.
            </div>
        </mat-error>
    </mat-form-field>
    <br>
    <mat-form-field>
        <input matInput placeholder="Tags" required #tags='ngModel' name='tags' [(ngModel)]='photoData.tags'
            #tags='ngModel'>
        <mat-error *ngIf="tags.invalid && (tags.dirty || tags.touched)">
            <div *ngIf="tags.errors.required">
                Tags is required.
            </div>
        </mat-error>
    </mat-form-field>
    <br>
    <button mat-raised-button type='submit'>Save</button>
</form>

This allows the user to upload a new photo and edit the description and tags fields. And in edit-photo-dialog.component.scss , we add:

#file {
  display: none;
}

so that the file upload input is hidden. We invoke the upload dialog with a click to the button and get the file with the handleFileInput handler.

Next we build the home page. In home-page.component.ts, we put:

import { Component, OnInit, ViewChild } from '@angular/core';
import { PhotoService } from '../photo.service';
import { environment } from 'src/environments/environment';
import { NgForm } from '@angular/forms';
@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.component.html',
  styleUrls: ['./home-page.component.scss']
})
export class HomePageComponent implements OnInit {
  photoUrls: string[] = [];
  query: any = <any>{};
  constructor(
    private photoService: PhotoService
  ) { }
  ngOnInit() {
    this.getPhotos();
  }
  getPhotos() {
    this.photoService.getPhotos()
      .subscribe(res => {
        this.photoUrls = (res as any).data.getPhotos.photos.map(p => {
          const pathParts = p.fileLocation.split('/');
          const photoPath = pathParts[pathParts.length - 1];
          return `${environment.photosUrl}/${photoPath}`;
        });
      })
  }
  searchPhotos(searchForm: NgForm) {
    if (searchForm.invalid) {
      return;
    }
    this.searchPhotosQuery();
  }
  searchPhotosQuery() {
    this.photoService.searchPhotos(this.query.search)
      .subscribe(res => {
        this.photoUrls = (res as any).data.searchPhotos.photos.map(p => {
          const pathParts = p.fileLocation.split('/');
          const photoPath = pathParts[pathParts.length - 1];
          return `${environment.photosUrl}/${photoPath}`;
        });
      })
  }
}

to get the photos that the user saved and allow users to search with the searchPhotosQuery function. We will call the photoService, which uses the Apollo client to make the request.

In home-page.component.html, we put:

<form #searchForm='ngForm' (ngSubmit)='searchPhotos(searchForm)'>
    <mat-form-field>
        <input matInput placeholder="Search Photos" required #search='ngModel' name='search' [(ngModel)]='query.search'>
        <mat-error *ngIf="search.invalid && (search.dirty || search.touched)">
            <div *ngIf="search.errors.required">
                Search query is required.
            </div>
        </mat-error>
    </mat-form-field>
    <br>
    <button mat-raised-button type='submit'>Search</button>
</form>
<br>
<mat-grid-list cols="3" rowHeight="1:1">
    <mat-grid-tile *ngFor='let p of photoUrls'>
        <img [src]='p' class="tile-image">
    </mat-grid-tile>
</mat-grid-list>

to display photos in a grid and let users search photos with a text input.

In home-page.component.scss, we add:

.tile-image {
  width: 100%;
  height: auto;
}

to stretch the image to fit in the grid.

Next in the reducer folder, we create two files, menu-reducer.ts and photos-reducer.ts, to make the reducers to store the state of our app. In menu-reducer.ts, we put:

const TOGGLE_MENU = 'TOGGLE_MENU';
function menuReducer(state, action) {
    switch (action.type) {
        case TOGGLE_MENU:
            state = action.payload;
            return state;
        default:
            return state
    }
}
export { menuReducer, TOGGLE_MENU };

And similarly in photos-reducer.ts, we add:

const SET_PHOTOS = 'SET_PHOTOS';
function photosReducer(state, action) {
    switch (action.type) {
        case SET_PHOTOS:
            state = action.payload;
            return state;
        default:
            return state
    }
}
export { photosReducer, SET_PHOTOS };

This stores the states of the left side menu and photos. In reducers/index.ts , we put:

import { menuReducer } from './menu-reducer';
import { photosReducer } from './photos-reducer';
export const reducers = {
  menu: menuReducer,
  photos: photosReducer
};

This allows the reducers to be included in our app module, allowing us to manipulate the state.

Next in the top-bar.component.ts, we put:

import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { TOGGLE_MENU } from '../reducers/menu-reducer';
@Component({
  selector: 'app-top-bar',
  templateUrl: './top-bar.component.html',
  styleUrls: ['./top-bar.component.scss']
})
export class TopBarComponent implements OnInit {
  menuOpen: boolean;
  constructor(
    private store: Store<any>
  ) {
    store.pipe(select('menu'))
      .subscribe(menuOpen => {
        this.menuOpen = menuOpen;
      })
  }
  ngOnInit() {
  }
  toggleMenu() {
    this.store.dispatch({ type: TOGGLE_MENU, payload: !this.menuOpen });
  }
}

It has a toggleMenu function to toggle the menu state and store the state.

In top-bar.component.html, we put:

<mat-toolbar>
    <a (click)='toggleMenu()' class="menu-button">
        <i class="material-icons">
            menu
        </i>
    </a>
    Image Gallery App
</mat-toolbar>

This shows the toolbar.

In top-bar.component.scss, we add:

.menu-button {
  margin-top: 6px;
  margin-right: 10px;
  cursor: pointer;
}
.menu-button {
  color: white;
}
.mat-toolbar-row,
.mat-toolbar-single-row {
  height: 64px;
  background-color: #fc036b;
  color: white;
}

This makes the spacing look better.

In upload-page.component.ts, we put:

import { Component, OnInit, ViewChild } from '@angular/core';
import { PhotoService } from '../photo.service';
import { environment } from 'src/environments/environment';
import { MatDialog } from '@angular/material';
import { EditPhotoDialogComponent } from '../edit-photo-dialog/edit-photo-dialog.component';
import { Store, select } from '@ngrx/store';
import { SET_PHOTOS } from '../reducers/photos-reducer';
import { NgForm } from '@angular/forms';
@Component({
  selector: 'app-upload-page',
  templateUrl: './upload-page.component.html',
  styleUrls: ['./upload-page.component.scss']
})
export class UploadPageComponent implements OnInit {
  photoData: any = <any>{};
  photoArrayData: any[] = [];
  page: number = 1;
  totalPhotos: number = 0;
  @ViewChild('photoUpload', null) photoUpload: any;
  displayedColumns: string[] = [
    'photoUrl',
    'description',
    'tags',
    'edit',
    'delete'
  ]
  constructor(
    private photoService: PhotoService,
    public dialog: MatDialog,
    private store: Store<any>
  ) {
    store.pipe(select('photos'))
      .subscribe(photos => {
        this.photoArrayData = photos;
      })
  }
  ngOnInit() {
    this.getPhotos();
  }
  clickUpload() {
    this.photoUpload.nativeElement.click();
  }
  handleFileInput(files) {
    console.log(files);
    this.photoData.file = files[0];
  }
save(uploadForm: NgForm) {
    if (uploadForm.invalid || !this.photoData.file) {
      return;
    }
    const {
      file,
      description,
      tags
    } = this.photoData;
    this.photoService.addPhoto(file, description, tags)
      .subscribe(res => {
        this.getPhotos();
      })
  }
  getPhotos() {
    this.photoService.getPhotos(this.page)
      .subscribe(res => {
        const photoArrayData = (res as any).data.getPhotos.photos.map(p => {
          const { id, description, tags } = p;
          const pathParts = p.fileLocation.split('/');
          const photoPath = pathParts[pathParts.length - 1];
          return {
            id,
            description,
            tags,
            photoUrl: `${environment.photosUrl}/${photoPath}`
          }
        });
        this.page = (res as any).data.getPhotos.page;
        this.totalPhotos = (res as any).data.getPhotos.totalPhotos;
        this.store.dispatch({ type: SET_PHOTOS, payload: photoArrayData });
      })
  }
  openEditDialog(index: number) {
    const dialogRef = this.dialog.open(EditPhotoDialogComponent, {
      width: '70vw',
      data: this.photoArrayData[index] || {}
    })
dialogRef.afterClosed().subscribe(result => {
      console.log('The dialog was closed');
    });
  }
  deletePhoto(index: number) {
    const { id } = this.photoArrayData[index];
    this.photoService.deletePhoto(id)
      .subscribe(res => {
        this.getPhotos();
      })
  }
}

This lets people upload their photos, open a dialog to edit, or delete photos.

It has a file input to take a file object and calls the photoService to make the GraphQL requests to the API to manipulate the photo table entries. In upload-page.component.html, we put:

<div class="center">
    <h1>Manage Files</h1>
</div>
<h2>Add Photo</h2>
<form #photoForm='ngForm' (ngSubmit)='save(photoForm)'>
    <div>
        <input type="file" id="file" (change)="handleFileInput($event.target.files)" #photoUpload>
        <button mat-raised-button (click)='clickUpload()' type='button'>
            Upload Photo
        </button>
        {{photoData?.file?.name}}
    </div>
    <mat-form-field>
        <input matInput placeholder="Description" required #description='ngModel' name='description'
            #description='ngModel' [(ngModel)]='photoData.description'>
        <mat-error *ngIf="description.invalid && (description.dirty || description.touched)">
            <div *ngIf="description.errors.required">
                Description is required.
            </div>
        </mat-error>
    </mat-form-field>
    <br>
    <mat-form-field>
        <input matInput placeholder="Tags" required #tags='ngModel' name='tags' [(ngModel)]='photoData.tags'
            #tags='ngModel'>
        <mat-error *ngIf="tags.invalid && (tags.dirty || tags.touched)">
            <div *ngIf="tags.errors.required">
                Tags is required.
            </div>
        </mat-error>
    </mat-form-field>
    <br>
    <button mat-raised-button type='submit'>Save</button>
</form>
<br>
<h2>Manage Photos</h2>
<table mat-table [dataSource]="photoArrayData" class="mat-elevation-z8">
    <ng-container matColumnDef="photoUrl">
        <th mat-header-cell *matHeaderCellDef> Photo </th>
        <td mat-cell *matCellDef="let photo">
            <img [src]='photo.photoUrl' class="photo">
        </td>
    </ng-container>
<ng-container matColumnDef="description">
        <th mat-header-cell *matHeaderCellDef> Description </th>
        <td mat-cell *matCellDef="let photo"> {{photo.description}} </td>
    </ng-container>
<ng-container matColumnDef="tags">
        <th mat-header-cell *matHeaderCellDef> Tags </th>
        <td mat-cell *matCellDef="let photo"> {{photo.tags}} </td>
    </ng-container>
<ng-container matColumnDef="edit">
        <th mat-header-cell *matHeaderCellDef> Edit </th>
        <td mat-cell *matCellDef="let photo; let i = index">
            <button mat-raised-button (click)='openEditDialog(i)'>Edit</button>
        </td>
    </ng-container>
<ng-container matColumnDef="delete">
        <th mat-header-cell *matHeaderCellDef> Delete </th>
        <td mat-cell *matCellDef="let photo; let i = index">
            <button mat-raised-button (click)='deletePhoto(i)'>Delete</button>
        </td>
    </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [length]="totalPhotos" [pageSize]="10" [pageSizeOptions]="[10]"
    (page)="page = $event.pageIndex + 1; getPhotos()">
</mat-paginator>

This shows a form to upload a photo and enter a description and tags with it, and it also displays a table row of photo data with edit and delete buttons in each row. Users can navigate through 10 photos per page with the paginator component at the bottom.

In upload-page.component.scss, we put:

#file {
  display: none;
}
table.mat-table,
.mat-paginator {
  width: 92vw;
}
.photo {
  width: 50px;
}

This hides the file upload input and changes the width of the table and paginator component to be the same.

Next in app-routing.module.ts, we put:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomePageComponent } from './home-page/home-page.component';
import { UploadPageComponent } from './upload-page/upload-page.component';
const routes: Routes = [
  { path: '', component: HomePageComponent },
  { path: 'upload', component: UploadPageComponent },
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

This routes the URLs to our pages we created.

In app.component.ts, we put:

import { Component, HostListener } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { TOGGLE_MENU } from './reducers/menu-reducer';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  menuOpen: boolean;
  constructor(
    private store: Store<any>,
  ) {
    store.pipe(select('menu'))
      .subscribe(menuOpen => {
        this.menuOpen = menuOpen;
      })
  }
  @HostListener('document:click', ['$event'])
  public onClick(event) {
    const isOutside = !event.target.className.includes("menu-button") &&
      !event.target.className.includes("material-icons") &&
      !event.target.className.includes("mat-drawer-inner-container")
    if (isOutside) {
      this.menuOpen = false;
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });
    }
  }
}

This adds the menu and the router-outlet element to display routes we designated in app-routing.module.ts.

In styles.scss, we put:

/* You can add global styles to this file, and also import other style files */
@import "[email protected]/material/prebuilt-themes/indigo-pink.css";
body {
  font-family: "Roboto", sans-serif;
  margin: 0;
}
form {
  mat-form-field {
    width: 95%;
    margin: 0 auto;
  }
}
.center {
  text-align: center;
}

This includes the Angular Material CSS into our code.

In index.html, we put:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Frontend</title>
  <base href="/">
  <link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

This changes the title and includes the Roboto font and Material icons in our app to display the icons.

Finally, in app.module.ts, we put:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {
  MatButtonModule,
  MatCheckboxModule,
  MatInputModule,
  MatMenuModule,
  MatSidenavModule,
  MatToolbarModule,
  MatTableModule,
  MatDialogModule,
  MatDatepickerModule,
  MatSelectModule,
  MatCardModule,
  MatFormFieldModule,
  MatGridListModule
} from '@angular/material';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { reducers } from './reducers';
import { TopBarComponent } from './top-bar/top-bar.component';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { HomePageComponent } from './home-page/home-page.component';
import { PhotoService } from './photo.service';
import { GraphQLModule } from './graphql.module';
import { UploadPageComponent } from './upload-page/upload-page.component';
import { MatPaginatorModule } from '@angular/material/paginator';
import { EditPhotoDialogComponent } from './edit-photo-dialog/edit-photo-dialog.component';
@NgModule({
  declarations: [
    AppComponent,
    TopBarComponent,
    HomePageComponent,
    UploadPageComponent,
    EditPhotoDialogComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    MatButtonModule,
    StoreModule.forRoot(reducers),
    BrowserAnimationsModule,
    MatButtonModule,
    MatCheckboxModule,
    MatFormFieldModule,
    MatInputModule,
    MatMenuModule,
    MatSidenavModule,
    MatToolbarModule,
    MatTableModule,
    HttpClientModule,
    MatDialogModule,
    MatDatepickerModule,
    MatSelectModule,
    MatCardModule,
    MatGridListModule,
    GraphQLModule,
    MatPaginatorModule
  ],
  providers: [
    PhotoService
  ],
  bootstrap: [AppComponent],
  entryComponents: [
    EditPhotoDialogComponent
  ]
})
export class AppModule { }

This includes everything we need to run the app module. Note that we have EditPhotoDialogComponent in entryComponents. This is required for displaying the dialog box in another element.

After all that work, we get the following:

The API I wish JavaScript GraphQL implementations supported

The API I wish JavaScript GraphQL implementations supported

The API I wish JavaScript GraphQL implementations supported - The GraphQL schema language is great! It is certainly the best way to communicate anything about a GraphQL service. No wonder all documentations now use it!

The API I wish JavaScript GraphQL implementations supported - The GraphQL schema language is great! It is certainly the best way to communicate anything about a GraphQL service. No wonder all documentations now use it!

The Schema Language

Imagine that you’re building a blog app (with GraphQL) that has "Articles" and "Comments" . You can start thinking about its API schema by basing it on what you plan for its UI. For example, the main page will probably have a list of articles and an item on that list might display a title, subtitle, author’s name, publishing date, length (in reading minutes), and a featured image. A simplified version of Medium itself if you may:

We can use the schema-language to plan what you need so far for that main page. A basic schema might look like:

type Query {
  articleList: [Article!]!
}
type Article {
  id: ID!
  title: String!
  subTitle: String
  featuredImageUrl: String
  readingMinutes: Int!
  publishedAt: String!
  author: Author!
}
type Author {
  name: String!
}

When a user navigates to an article, they’ll see the details of that article. We’ll need the API to support a way to retrieve an Article object by its id. Let’s say an article can also have rich UI elements like headers and code snippets. We would need to support a rich-text formatting language like Markdown. We can make the API return an article’s content in either Markdown or HTML through a field argument (format: HTML). Let’s also plan to display a "likes" counter in that view.

Put all these ideas on paper! The schema language is the most concise structured way to describe them:

type Query {
  # ...
  article(id: String!): Article!
}
enum ContentFormat {
  HTML
  MARKDOWN
}
type Article {
  # ...
  content(format: ContentFormat): String!
  likes: Int!
}

The one article’s UI view will also display the list of comments available on an article. Let’s keep the comment UI view simple and plan it to have a text content and an author name fields:

type Article {
  # ...
  commentList: [Comment!]!
}
type Comment {
  id: ID!
  content: String!
  author: Author!
}

Let’s focus on just these features. This is a good starting point that’s non-trivial. To offer these capabilities we’ll need to implement custom resolving logic for computed fields like content(format: HTML) and readingMinutes. We’ll also need to implement 1–1 and 1-many db relationships.

Did you notice how I came up with the whole schema description so far just by thinking in terms of the UI. How cool is that? You can give this simple schema language text to the front-end developers on your team and they can start building the front-end app right away! They don’t need to wait for your server implementation. They can even use some of the great tools out there to have a mock GraphQL server that resolves these types with random test data.

The schema is often compared to a contract. You always start with a contract.## Building a GraphQL Schema

When you’re ready to start implementing your GraphQL service, you have 2 main options (in JavaScript) today:

  1. You can "build" a non-executable schema using the full schema language text that we have and then attach a set of resolver functions to make that schema executable. You can do that with GraphQL.js itself or with Apollo Server. Both support this method which is commonly known as "schema-first" or "SDL-first". I’ll refer to it here as the "full-schema-string method".
  2. You can use JavaScript objects instantiated from the various constructor classes that are available in the GraphQL.js API (like GraphQLSchema, GraphQLObjectType, GraphQLUnionType, and many others). In this approach, you don’t use the schema-language text at all. You just create objects. This method is commonly known as "code-first" or "resolvers-first" but I don’t think these names fairly represent it. I’ll refer to it here as the "object-based method".

Both approaches have advantages and disadvantages.

The schema language is a great programming-language-agnostic way to describe a GraphQL schema. It’s a human-readable format that’s easy to work with. The frontend people on your team will absolutely love it. It enables them to participate in the design of the API and, more importantly, start using a mocked version of it right away. The schema language text can serve as an early version of the API documentation.

However, completely relying on the full schema language text to create a GraphQL schema has a few drawbacks. You’ll have to put in some effort to make the code modularized and clear and you have to rely on coding patterns and tools to keep the schema-language text consistent with the tree of resolvers (AKA resolvers map). These are solvable problems.

The biggest problem I see with the full-schema-string method is that you lose some flexibility in your code. You don’t have objects associated with types. You just have strings! And although these strings make your types more readable, in many cases you’ll need the flexibility over the readability.

The object-based method is flexible and easier to extend and manage. It does not suffer from any of the mentioned problems. You have to be modular with it because your schema is a bunch of objects. You also don’t need to merge modules together because these objects are designed and expected to work as a tree.

The only problem I see with the object-based method is that you have to deal with a lot more code around what’s important to manage in your modules (types and resolvers). A lot of developers see that as "noise" and you can’t blame them. We’ll work through an example to see that.

If you’re creating a small-scope and well-defined GraphQL service, using the full-schema-string method is probably okay. However, in bigger and more agile projects I think the more flexible and more powerful object-based method is the way to go.

You should still leverage the schema-language text even if you’re using the object-based method. At jsComplete, we use the object-based method but every time the schema is built we use the graphql.printSchema function to write the complete schema to a file. We commit and track that file in the Git repository of the project and that proved to be a very helpful practice!
To compare the 2 methods, I’ve implemented an executable schema for the blog example we started with using both of them. I’ve omitted some code for brevity but kept what matters for the comparison.

The full-schema-string method

We start with the schema-language text which defines 3 main custom types (Article, Comment, and Author). The fields under the main Query type are article and articleList which will directly resolve objects from the database. However, since the GraphQL schema we planned has custom features around an article object and since we have relations that we need to resolve as well we’ll need to have custom resolvers for the 3 main custom GraphQL types.

Here are a few screenshots for the code I wrote to represent the full-schema-string method. I’ve used Apollo Server here but this is also possible with vanilla GraphQL.js (and a bit more code).

Please note that this is just ONE way of implementing the full-schema-string method for this service. There are countless other ways. I am just presenting the simplest modular way here to help us understand the true advantages and disadvantages.

This is nice! We can see the types in the schema in one place. It’s clear where the schema starts. We’re able to modularize the code by type/feature.

This again is really great! Resolvers are co-located with the types they implement. There is no noise. This file beautifully contains what matters in a very readable format. I love it!

The modularity here is only possible with Apollo Server. If we’re to do this with vanilla GraphQL.js we will have to monkey with data objects to make them suitable to be a "resolvers tree". The mixing between the data structures and the resolvers graph is not ideal.
So what’s the downside here?

If you use this method then all your types have to be written in that certain way that relies on the schema language text. You have less flexibility. You can’t use constructors to create some types when you need to. You’re locked down to this string-based approach.

If you’re okay with that then ignore the rest of this article. Just use this method. It is so much cleaner than the alternative.

The object-based method

Let’s now look at the object-based approach. Here’s the starting point of an executable schema built using that method:

We don’t need a separate resolvers object. Resolvers are part of the schema object itself. That makes them easier to maintain. This code is also easier to programmatically extend and analyze!

It’s also so much more code that’s harder to read and reason about! Wait until you see the rest of the code. I couldn’t take the Article type screenshot on the laptop screen. I had to use a bigger screen.

No wonder the full-schema-string method is popular! There is certainly a lot of "noise" to deal with here. Types are not clear at first glance. Custom resolvers are mixed in one big configuration object.

My favorite part is when you need to create a non-null list of non-null items like [Article!]!. Did you see what I had to write?

new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Article))),

However, while this is indeed a lot more code that’s harder to understand, it is still a better option than having one big string (or multiple strings combined into one) and one big root resolvers object (or multiple resolvers objects combined into one). It’s better than having all the dependencies of your app managed in one single entry point.

There is a lot of power in modularizing your code using objects (that may depend on each other). It’s cleaner that way and it also makes writing tests and validations easier. You get more helpful error messages when you debug problems. Modern editors can provide more helpful hints in general. Most importantly, you have a lot more flexibility to do anything with these objects. The GraphQL.js constructors API itself also uses JavaScript objects. There is so much you can do with them.

But the noise is real too.

The object-based method without the noise

I am sticking with the object-based method but I sure wish the JavaScript GraphQL implementations had a better API that can give us some of the power of the full-schema-string method.

Wouldn’t be nice if we can write the Article type logic exactly as we did in the full-schema-string method but in a way that generates the flexible GraphQLObjectType that we can plug into an object-based schema?

Something like:

Wouldn’t that be ideal? We get the benefits of the full-schema-string method for this type but with no lockdown! Other types in the system can be maintained differently. Maybe other types will be dynamically constructed using a different maker logic!

All we need to make this happen is a magical <strong>typeMakerMethod</strong> to take the parts that matter and transform them into the complete GraphQLObjectType for Article.

The typeMakerMethod will need to parse a string into an AST, use that to build a GraphQLObjectType, then merge the set of custom resolver functions with the fieldsconfiguration that’ll be parsed from the typeDef string.

I like a challenge so I dug a little bit deeper to see how hard would it be to implement the typeMakerMethod. I knew I couldn’t use the graphql.buildSchema function because it only parses one full schema string to make a non executable schema object. I needed a lower-level part that parses a string that has exactly ONE type and then attaches custom resolvers to it. So I started reading the source code of GraphQL.js to look for clues. A few cups of coffee later, I found some answers (in 2 places):

That’s the core method used in buildSchema to construct ONE type from a type definition node (which we can easily get by parsing the typeDef string).

And:

That’s how easy it is to extend an object type and attach any logic needed in fields and interfaces!

All I needed to do is put a few pieces together and the dream can be true.

I did.

Ladies and gentlemen. I present to you the magical "typeMakerMethod" (which I named objectType):

That’s it (in its most basic form)! This will take a typeDef string that defines a single GraphQL type, an object of resolvers and a map of dependencies (for that type), and it’ll return a GraphQLObjectType ready to be plugged into your object-based schema as if it was defined normally with the object constructor.

Now you can use the object-based method but you have the option to define SOME types using an approach similar to the full-schema-string method. You have the power.

What do you think of this approach? I’d love to hear your feedback!

Please note that the objectType code above is just the basic use case. There are many other use cases that require further code. For example, if the types have circular dependencies (articleauthorarticle) then this version of objectType will not work. We can delay the loading of the circular dependencies until we’re in the fields thunk (which is the current approach to solve this problem in the object-based method). We can also use the "extend" syntax to design the schema in a way that avoids circular dependencies in the first place. I’ve skipped this part to keep the example simple.> If you’d like to give it a spin I published a more polished version of objectTypeand a few other maker functions like it under the graphql-makers npm package.

How to get started Internationalization in JavaScript with NodeJS

How to get started Internationalization in JavaScript with NodeJS

Tutorial showing how to use the Intl JS API in NodeJS (i18n). We'll install a module to unlock the Intl API languages for Node and test out RelativeTimeFormat to translate and localise relative times in JavaScript.

Tutorial showing how to use the Intl JS API in NodeJS (i18n). We'll install a module to unlock the Intl API languages for Node and test out RelativeTimeFormat to translate and localise relative times in JavaScript. I'll tell you how to get started with the built-in internationalization library in JS for Node 12 and higher. We'll change the locale to see how the translation works and test different BCP 47 language tags.

Internationalization is a difficult undertaking but using the Intl API is an easy way to get started, it's great to see this new API in the JS language and available for use. Soon, you'll be able to have confidence using it in the browser as modern browsers support the major Intl features. Have a look at the browser compatibility charts to see which browsers and versions of node are supported.

Use Intl.RelativeTimeFormat for language-sensitive relative time formatting.
#javascript #nodejs #webdevelopment

MDN Documentation:

https://developer.mozilla.org/en-US/d...

Full ICU NPM package:

https://www.npmjs.com/package/full-icu

Creating a simple CRUD app with NodeJS, GraphQL and React

Creating a simple CRUD app with NodeJS, GraphQL and React

Make CRUD simple with Node, GraphQL, and React. GraphQ Lreduces the complexity of building APIs by abstracting all requests to a single endpoint. Unlike traditional REST APIs, it is declarative; whatever is requested is returned

GraphQL reduces the complexity of building APIs by abstracting all requests to a single endpoint. Unlike traditional REST APIs, it is declarative; whatever is requested is returned.

Of course, not all projects require GraphQL — it is merely a tool to consolidate data. It has well-defined schema, so we know for sure we won’t overfetch. But if we already have a stable RESTful API system where we rely on data from a single data source, we don’t need GraphQL.

For instance, let’s assume we are creating a blog for ourselves and we decide to store, retrieve, and communicate to data in a single MongoDB database. In this case, we are not doing anything architecturally complex, and we don’t need GraphQL.

On the other hand, let’s imagine we have a full-fledged product that relies on data from multiple sources (e.g., MongoDB, MySQL, Postgres, and other APIs). In this case, we should go for GraphQL.

For example, if we’re designing a portfolio site for ourselves and we want data from social media and GitHub (to show contributions), and we also have our own database to maintain a blog, we can use GraphQL to write the business logic and schema. It will consolidate data as a single source of truth.

Once we have the resolver functions to dispatch the right data to the front end, we will easily be able to manage data within a single source. In this article, we’re going to implement simple end-to-end CRUD operations with GraphQL.

CRUD with graphql-server

Setting up our server

We are going to spin off a simple GraphQL server using express-graphql and get it connected to a MySQL database. The source code and the MySQL files are in this repository.

A GraphQL server is built on top of schema and resolvers. As a first step, we build a schema (defining types, queries, mutations, and subscriptions). This schema describes the whole app structure.

Secondly, for the stuff defined in the schema, we’re building respective resolvers to compute and dispatch data. A resolver maps actions with functions; for each query declared in typedef, we create a resolver to return data.

Finally, we complete server settings by defining an endpoint and passing configurations. We initialize /graphql as the endpoint for our app. To the graphqlHTTP middleware, we pass the built schema and root resolver.

Along with the schema and root resolver, we’re enabling the GraphiQL playground. GraphiQL is an interactive in-browser GraphQL IDE that helps us play around with the GraphQL queries we build.

var express = require('express');
var graphqlHTTP = require('express-graphql');
var { buildSchema } = require('graphql');

var schema = buildSchema(`
  type Query {
    hello: String
  }
`);

var root = {
  hello: () => "World"
};

var app = express();

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

app.listen(4000);

console.log('Running a GraphQL API server at localhost:4000/graphql');

Once the server is good to go, running the app with node index.js will start the server on http://localhost:4000/graphql. We can query for hello and get the string “World” as a response.

Connecting the database

I’m going to establish the connection with the MySQL database as shown below:

var mysql = require('mysql');

app.use((req, res, next) => {
  req.mysqlDb = mysql.createConnection({
    host     : 'localhost',
    user     : 'root',
    password : '',
    database : 'userapp'
  });
  req.mysqlDb.connect();
  next();
});

We can connect multiple databases/sources and get them consolidated in the resolvers. I’m connecting to a single MySQL database here. The database dump I’ve used for this article is in the GitHub repository.

Reading and writing data with GraphQL

We use queries and mutations to read and modify data in data-sources. In this example, I’ve defined a generic queryDB function to help query the database.

Queries

All the SELECT statements (or read operations) to list and view data goes into the type Query typedef. We have two queries defined here: one to list all the users in the database, and another to view a single user by id.

  1. Listing data: To list users, we’re defining a GraphQL schema object type called User, which represents what we can fetch or expect from the getUsers query. We then define the getUsers query to return an array of users.
  2. Viewing a single record: To view a single record, we’re taking id as an argument with the getUserInfo query we have defined. It queries for that particular id in the database and returns the data to the front end.

Now that we have put together the queries to fetch all records and to view record by id, when we try to query for users from GraphiQL, it will list an array of users on the screen! 🙂

var schema = buildSchema(`
  type User {
    id: String
    name: String
    job_title: String
    email: String
  }
  type Query {
    getUsers: [User],
    getUserInfo(id: Int) : User
  }
`);

const queryDB = (req, sql, args) => new Promise((resolve, reject) => {
    req.mysqlDb.query(sql, args, (err, rows) => {
        if (err)
            return reject(err);
        rows.changedRows || rows.affectedRows || rows.insertId ? resolve(true) : resolve(rows);
    });
});

var root = {
  getUsers: (args, req) => queryDB(req, "select * from users").then(data => data),
  getUserInfo: (args, req) => queryDB(req, "select * from users where id = ?", [args.id]).then(data => data[0])
};

Mutations

The write operations for the database — CREATE, UPDATE, DELETE — are generally defined under mutations. The mutations are executed in a sequential manner by the GraphQL engine. Queries are executed parallelly.

  1. Creating data: We have defined a mutation, createUser, that takes the specified arguments to create data in the MySQL database.
  2. Updating or deleting data: Similar to viewing a record, update (updateUserInfo) and delete (deleteUser) take id as a param and modify the database.

The functions resolve with a boolean to indicate whether the change happened or not.

var schema = buildSchema(`
  type Mutation {
    updateUserInfo(id: Int, name: String, email: String, job_title: String): Boolean
    createUser(name: String, email: String, job_title: String): Boolean
    deleteUser(id: Int): Boolean
  }
`);

var root = {
  updateUserInfo: (args, req) => queryDB(req, "update users SET ? where id = ?", [args, args.id]).then(data => data),
  createUser: (args, req) => queryDB(req, "insert into users SET ?", args).then(data => data),
  deleteUser: (args, req) => queryDB(req, "delete from users where id = ?", [args.id]).then(data => data)
};

Now that we have set and sorted the server side of things, let’s try and connect the back end to our React app.

CRUD with graphql-client

Once we have the server in place, creating client logic to display and mutate data is easy. Apollo Client helps in state management and caching. It is also highly abstracted and quick: all of the logic for retrieving your data, tracking loading and error states, and updating UI is encapsulated by the useQuery Hook.

Connecting to graphql-server

I have created a CRA boilerplate and have installed GraphQL, apollo-boost, and @apollo/react-hooks. We initialize Apollo Client and get it hooked to React.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from '@apollo/react-hooks';

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql'
});

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root')
);

Reading and mutating data

I have managed all the GraphQL queries in the Queries folder of my source code. I’m going to request data from the server with the useQuery Hook, which is built on top of the React Hooks API. It helps in bringing in data into the UI.

GraphQL queries are generally wrapped in the gql function. gql helps convert query string into a query document. Here’s how we define queries in our app.

import { gql } from 'apollo-boost';

export const GET_USERS = gql`
  {
    getUsers {
      id,
      name,
      job_title,
      email
    }
  }
`;

export const VIEW_USERS = gql`
  query ($id: Int){
    getUserInfo(id: $id) {
      id,
      name,
      job_title,
      email
    }
  }
`;

export const ADD_USER = gql`
  mutation($name: String, $email: String, $job_title: String) {
    createUser (name: $name, email: $email, job_title: $job_title)
  }
`;

export const EDIT_USER = gql`
  mutation($id: Int, $name: String, $email: String, $job_title: String) {
    updateUserInfo (id: $id, name: $name, email: $email, job_title: $job_title)
  }
`;

export const DELETE_USER = gql`
  mutation($id: Int) {
    deleteUser(id: $id)
  }
`

Once ApolloProvider is set, we can request data from our GraphQL server. We pass the query we are trying to make to the useQuery Hook, and it will provide the result for us.

I’ve made two queries, with and without arguments, to show how we should be handling queries and mutations in the front end. useQuery tracks error and loading states for us and will be reflected in the associated object. Once the server sends the result, it will be reflected by the data property.

import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import { GET_USERS, VIEW_USERS } from "./Queries";
import { Card, CardBody, CardHeader, CardSubtitle, Spinner } from 'reactstrap';

function App() {
  const getAllUsers = useQuery(GET_USERS);
  const userInfo = useQuery(VIEW_USERS, { variables: { id: 1 }});
  if (getAllUsers.loading || userInfo.loading) return <Spinner color="dark" />;
  if (getAllUsers.error || userInfo.error) return <React.Fragment>Error :(</React.Fragment>;

  return (
    <div className="container">
      <Card>
        <CardHeader>Query - Displaying all data</CardHeader>
        <CardBody>
          <pre>
            {JSON.stringify(getAllUsers.data, null, 2)}
          </pre>
        </CardBody>
      </Card>
      <Card>
        <CardHeader>Query - Displaying data with args</CardHeader>
        <CardBody>
          <CardSubtitle>Viewing a user by id</CardSubtitle>
          <pre>
            {JSON.stringify(userInfo.data, null, 2)}
          </pre>
        </CardBody>
      </Card>
    </div>
  )
}

export default App;

Similar to querying, mutations will use the same useQuery Hook and will pass data as variables into the query.

const deleteMutation = useQuery(DELETE_USER, { variables: { id: 8 }});
const editMutation = useQuery(EDIT_USER, { variables: { id: 9, name: "Username", email: "email", job_title: "job" }});
const createMutation = useQuery(ADD_USER, { variables: { name: "Username", email: "email", job_title: "job" }});

Monitor Failed and Slow GraphQL Requests in Production

While GraphQL has some features for debugging requests and responses, making sure GraphQL continues to serve resources to your app in production is where things get tougher. If you’re interested in ensuring requests to the backend or 3rd party services are successful,

LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, and slow network requests as well as logs Redux, NgRx. and Vuex actions/state. Start monitoring for free.

Conclusion

Ta-da! We just did end-to-end CRUD operations with GraphQL. On the client side, reading and mutating data has become very simple after the introduction of React Hooks. Apollo Client also provides provisions for authentication, better error handling, caching, and optimistic UI.

Subscriptions is another interesting concept in GraphQL. With this application as boilerplate, we can keep experimenting with other concepts like these!

Happy coding!