Fullstack GraphQL NodeJS with React

 

What is GraphQL?

  • "QL" stands for query language - it describes the data and requests to API

 

  • It's an alternative to REST for developing a service layer

REST

GraphQL

Why GraphQL

  • A strongly-typed schema means data is predictable
  • Clients can request only data they need, no server-side changes needed
  • Clients send one request, reducing workload for frontend
  • Automatic API documentation creation with graphiql

What we'll cover

  • Overview of the frontend React app
  • Setting up the graphql server (NodeJS)
  • Querying data from server
  • Connecting frontend to server
  • Query with variables
  • Mutation with Input objects

...additionally (if there's time)

Demo/Live-Coding

[
  {
    "quote_id": "rkz1GwOOM",
    "movie_id": "tt0468569",
    "description": "I'm Batman"
  },
  {
    "quote_id": "rkz1GwOAS",
    "movie_id": "tt0468569",
    "description": "Why so serious?"
  },
]
[
  {
    "movie_id": "tt0468569",
    "title": "The Dark Knight",
    "year": "2008",
    "plot": "When the menace known as The Joker emerges from his mysterious past, he wreaks havoc and chaos on the people of Gotham. The Dark Knight must accept one of the greatest psychological and physical tests of his ability to fight injustice.",
    "poster": "https://m.media-amazon.com/images/M/MV5BMTMxNTMwODM0NF5BMl5BanBnXkFtZTcwODAyMTk2Mw@@._V1_SX300.jpg",
    "rated": "PG-13",
    "runtime": "152 min"
  },
]

movies.json

quotes.json

MovieList.js

Movie.js

MovieCreate.js

...Backend

Code

const { ApolloServer, gql } = require('apollo-server');
const fs = require('fs')
const moviesData = JSON.parse(fs.readFileSync('./data/movies.json', 'utf8'))
const quotesData = JSON.parse(fs.readFileSync('./data/quotes.json', 'utf8'))

const resolvers = {
  Mutation: {
    createMovie:(_,{input}) => {
      // let result = db.movies.create({...input})
      // return result
      input.movie_id = Math.random().toString(36).substring(7); // random string
      return input;
    }
  },
  Query: {
    movies: () => {
      return moviesData
    },
    movie: (_, {movie_id}) => {
      return moviesData.find(x => x.movie_id === movie_id)
    }
  },
  Movie: {
    quotes: (movie) => {
      return quotesData.filter(y => y.movie_id === movie.movie_id)
    }
  }
}

const typeDefs = gql`
  type Mutation {
    createMovie(input: MovieInput): Movie
  }
  type Query {
    movies: [Movie]
    movie(movie_id: ID): Movie
  }
  type Movie {
    movie_id: ID,
    title: String,
    year: String,
    plot: String,
    poster: String,
    rated: String,
    runtime: String,
    quotes: [Quote],
  }
  type Quote {
    quote_id: ID,
    description: String,
  }
  input MovieInput {
    title: String,
    year: String,
    plot: String,
  }
`;

const server = new ApolloServer({ typeDefs,resolvers });

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

Backend

1. Connect Database

const fs = require('fs')
const moviesData = JSON.parse(
  fs.readFileSync('./data/movies.json', 'utf8')
)
const quotesData = JSON.parse(
  fs.readFileSync('./data/quotes.json', 'utf8')
)

Backend

2. Create Apollo Graphql Server

const { ApolloServer, gql } = require('apollo-server');

const server = new ApolloServer({ typeDefs,resolvers });

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

Backend

const typeDefs = gql`
  type Query {
    movies: [Movie]
  }
  type Movie {
    movie_id: ID,
    title: String,
    year: String,
    plot: String,
    poster: String,
    rated: String,
    runtime: String,
    quotes: [Quote],
  }

  type Quote {
    quote_id: ID,
    description: String,
  }
`

3. Create typedef

[
  {
    "quote_id": "rkz1GwOOM",
    "movie_id": "tt0468569",
    "description": "I'm Batman"
  },
]
[
  {
    "movie_id": "tt0468569",
    "title": "The Dark Knight",
    "year": "2008",
    "plot": "When the menace known as The Joker emerges from his mysterious past, he wreaks havoc and chaos on the people of Gotham. The Dark Knight must accept one of the greatest psychological and physical tests of his ability to fight injustice.",
    "poster": "https://m.media-amazon.com/images/M/MV5BMTMxNTMwODM0NF5BMl5BanBnXkFtZTcwODAyMTk2Mw@@._V1_SX300.jpg",
    "rated": "PG-13",
    "runtime": "152 min"
  },
]
const typeDefs = gql`
  type Query {
    movies: [Movie]
  }
  type Movie {
    movie_id: ID,
    title: String,
    year: String,
    plot: String,
    poster: String,
    rated: String,
    runtime: String,
    quotes: [Quote],
  }

  type Quote {
    quote_id: ID,
    description: String,
  }
`

Backend

const resolvers = {
  Query: {
    movies: () => {
      return moviesData
    },
    movie: (_, {movie_id}) => {
      return moviesData.find(x => x.movie_id === movie_id)
    }
  },
}

4. Create Resolver

Backend

 

5. Run backend. Test in localhost:4000/graphql

query {
  movies {
    title
    year
  }
}

Backend

 

7. More Advanced Stuff

const resolvers = {
  Mutation: {
    createMovie:(_,{input}) => {
      input.movie_id = Math.random().toString(36).substring(7); // random string
      return input;
    }
  },
  Query: {
    movies: () => {
      return moviesData
    },
    movie: (_, {movie_id}) => {
      return moviesData.find(x => x.movie_id === movie_id)
    }
  },
  Movie: {
    quotes: (movie) => {
      return quotesData.filter(y => y.movie_id === movie.movie_id)
    }
  }
}

...Frontend

App.js

import React from 'react';
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';
import { ApolloProvider } from '@apollo/react-hooks';
import ApolloClient from 'apollo-boost';
// Components
import {Movie} from './components/Movie';
import {MovieCreate} from "./components/MovieCreate";
import {MovieList} from "./components/MovieList";

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

const App = () => {
  return (
    <ApolloProvider client={client}>
      <Router>
        <div className="App">
          <div>
            <Link className="title" to={`/`}>Movie Quotes</Link>
          </div>
          <Switch>
            <Route exact path="/" component={MovieList}/>
            <Route exact path="/movie/create" component={MovieCreate}/>
            <Route path="/movie/:movieId" component={Movie}/>
          </Switch>
        </div>
      </Router>
    </ApolloProvider>
  );
}

export default App;

MovieList.js

import React from 'react'
import {Link} from "react-router-dom"
import movieData from '../data/movies.json'
import quotesData from '../data/quotes.json'

export const MovieList = () => {
  return (
    <div className="movie-wrapper">
      <Link className="btn-add-new" to={`/movie/create`}>+ Add New Movie</Link>
      <div className="movie-list">
        {movieData.map(movie => {
          return (
            <div className="movie-item">
              <img src={movie.poster} />
              <section>
                <h3>{movie.title}</h3>
                <p>{movie.year}</p>
                <ul>
                  {quotesData.map(quote => {
                    if (quote.movie_id === movie.movie_id) {
                      return (
                        <li key={quote.quote_id}>
                          {quote.description}
                        </li>
                      )
                    }
                  })}
                </ul>
                <Link className="btn-normal" to={`/movie/${movie.movie_id}`}>Movie Details</Link>
              </section>
            </div>
          )
        })}
      </div>
    </div>
  )
}

Movie.js

import React from 'react'
import movieData from '../data/movies.json';
import quotesData from '../data/quotes.json';

export const Movie = (props) =>{
  const {movieId} = props.match.params; //windows
  const filteredMovie = movieData.find(x=> x.movie_id === movieId)
  const quotes = quotesData.filter(y => y.movie_id === movieId)
  return (
    <div className="movie-item single-movie">
      <img src={filteredMovie.poster} />
      <section>
        <h3>{filteredMovie.title}</h3>
        <p>{filteredMovie.year}</p>
        <ul>
          {quotes.map(quote => {
            if (quote.movie_id === filteredMovie.movie_id) {
              return (
                <li key={quote.quote_id}>
                  {quote.description}
                </li>
              )
            }
          })}
        </ul>
        <p>PLOT: {filteredMovie.plot}</p>
      </section>
    </div>
  )
}

MovieCreate.js

import React, {useState} from 'react'
import Swal from 'sweetalert2'

export const MovieCreate = () =>{
  const [plot, setPlot] = useState('');
  const [title, setTitle] = useState('');
  const [year, setYear] = useState('');
  const onSubmit = event => {
    event.preventDefault();
    Swal.fire({
      type: 'success',
      html: `
       <p>Title: ${title}</p>
       <p>Year: ${year}</p>
       <p>Plot: ${plot}</p>`
    })
  }
  return (
    <div className="movie-create">
      <form onSubmit={onSubmit}>
        <label>Title</label>
        <input
          type="text"
          onChange={(e) => setTitle(e.target.value)}
        />
        <label>Year</label>
        <input
          type="text"
          onChange={(e) => setYear(e.target.value)} 
        />
        <label>Plot</label>
        <textarea 
          type="text" 
          onChange={(e) => setPlot(e.target.value)}
        />
        <button className="btn-normal" type="submit">Submit</button>
      </form>
    </div>
  )
}

Frontend

1. Create the ApolloClient

import ApolloClient from 'apollo-boost';

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

Frontend

2. ApolloProvider Wrapper

import { ApolloProvider } from '@apollo/react-hooks';

const App = () => {
  return (
    <ApolloProvider client={client}>
      <div>Hello World!</div>
    </ApolloProvider>
   )
}

Frontend

3. Create GraphQL String

import { gql } from 'apollo-boost';

const GET_MOVIES = gql`
  query {
    movies {
      movie_id
      title
      year
      plot
      poster
      quotes {
        quote_id
        description
      }
    }
  }
`;

Frontend

4. useQuery hook

import { useQuery } from '@apollo/react-hooks';

export const MovieList = () => {
  const {loading, error, data} = useQuery(GET_MOVIES);
  if (loading) return <p>Loading...</p>
  if (error) return <p>Error ...</p>
  
  return (
    <div>
      {data.movies.map(movie =>{
        <h2>{movie.title}</h2>
      })}
    </div>
  )
}

Frontend

5. More Advanced Frontend

...Summary

1.Frontend
2.Backend
query {
  movies {
    title
    year
    quotes {
      description
    }
  }
}
type Query {
  movies: [Movie]
}
3.Backend
const resolvers = {
  Query: {
    movies: () => {
      return moviesData
    },
  },
  Movie: {
    quotes: (movie) => {
      return quotesData.filter(y => y.movie_id === movie.movie_id)
    }
  }
}
5.Frontend
type Movie {
  movie_id: ID,
  title: String,
  year: String,
  plot: String,
  poster: String,
  rated: String,
  runtime: String,
  quotes: [Quote],
}

type Quote {
  quote_id: ID,
  description: String,
}
4.Backend
const resolvers = {
  Query: {
    movies: () => {
      return moviesData
    },
  },
  Movie: {
    quotes: (movie) => {
      return quotesData.filter(y => y.movie_id === movie.movie_id)
    }
  }
}

...Questions

Fullstack GraphQL NodeJS with React

Fullstack GraphQL NodeJS React

By Vincent Tang

Fullstack GraphQL NodeJS React

An overview of GraphQL NodeJS with a movie quote app

  • 1,661