├── .gitignore ├── README.md ├── api ├── src │ ├── db │ │ ├── db.json │ │ ├── index.js │ │ ├── pet.js │ │ ├── schema.json │ │ └── user.js │ ├── resolvers.js │ ├── schema.js │ └── server.js └── tests │ ├── mutations.test.js │ ├── queries.test.js │ └── resolvers.test.js ├── client ├── .babelrc ├── index.html └── src │ ├── client.js │ ├── components │ ├── App.js │ ├── Error.js │ ├── Header.js │ ├── Loader.js │ ├── NewPet.js │ └── PetBox.js │ ├── index.css │ ├── index.js │ └── pages │ └── Pets.js ├── db.json ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | dist 65 | coverage 66 | .cache 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fullstack GraphQL 2 | > Learn how to use GraphQL with Node and React 3 | 4 | This repository contains code used in two [Frontend Masters](https://frontendmasters.com) courses: [Server-Side GraphQL in Node.js](https://frontendmasters.com/courses/server-graphql-nodejs/) and [Client-Side GraphQL in React](https://frontendmasters.com/courses/client-graphql-react/) 5 | 6 | ## What you'll need 7 | * Node version 8.17.0 8 | 9 | ## Client-Side GraphQL in React 10 | After cloning the repository, make sure to switch to the `client` branch before running the application: 11 | ```bash 12 | git clone https://github.com/FrontendMasters/fullstack-graphql.git 13 | git checkout client -- 14 | npm install 15 | npx yarn app 16 | ``` 17 | 18 | ## Solutions 19 | The solution branch has the completed course fo reference. There is no one way to finish this course. 20 | `git checkout solution` 21 | -------------------------------------------------------------------------------- /api/src/db/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "id": "jBWMVGjm50l6LGwepDoty", 4 | "username": "frontendmaster" 5 | }, 6 | "pet": [] 7 | } 8 | -------------------------------------------------------------------------------- /api/src/db/index.js: -------------------------------------------------------------------------------- 1 | const low = require('lowdb') 2 | const FileSync = require('lowdb/adapters/FileSync') 3 | 4 | const adapter = new FileSync('api/src/db/db.json') 5 | const db = low(adapter) 6 | 7 | const createPetModel = require('./pet') 8 | const createUserModel = require('./user') 9 | 10 | module.exports = { 11 | models: { 12 | Pet: createPetModel(db), 13 | User: createUserModel(db), 14 | }, 15 | db 16 | } 17 | -------------------------------------------------------------------------------- /api/src/db/pet.js: -------------------------------------------------------------------------------- 1 | const nanoid = require('nanoid') 2 | 3 | const createPetModel = db => { 4 | return { 5 | findMany(filter) { 6 | return db.get('pet') 7 | .filter(filter) 8 | .value() 9 | }, 10 | 11 | findOne(filter) { 12 | return db.get('pet') 13 | .find(filter) 14 | .value() 15 | }, 16 | 17 | create(pet) { 18 | const newPet = {id: nanoid(), createdAt: Date.now(), ...pet} 19 | 20 | db.get('pet') 21 | .push(newPet) 22 | .write() 23 | 24 | return newPet 25 | } 26 | } 27 | } 28 | 29 | module.exports = createPetModel 30 | -------------------------------------------------------------------------------- /api/src/db/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "id": "string", 4 | "username": "string" 5 | }, 6 | "pet":{ 7 | "id": "string", 8 | "createdAt": "number", 9 | "name": "string", 10 | "type": "string" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /api/src/db/user.js: -------------------------------------------------------------------------------- 1 | const nanoid = require('nanoid') 2 | 3 | const createUserModel = db => { 4 | return { 5 | findOne() { 6 | return db.get('user') 7 | .value() 8 | }, 9 | 10 | create(user) { 11 | const newUser = {id: nanoid(), createdAt: Date.now(), ...user} 12 | db.set('user', newUser) 13 | .write() 14 | 15 | return newUser 16 | } 17 | } 18 | } 19 | 20 | module.exports = createUserModel 21 | -------------------------------------------------------------------------------- /api/src/resolvers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Here are your Resolvers for your Schema. They must match 3 | * the type definitions in your scheama 4 | */ 5 | 6 | module.exports = { 7 | Query: { 8 | 9 | }, 10 | Mutation: { 11 | 12 | }, 13 | Pet: { 14 | img(pet) { 15 | return pet.type === 'DOG' 16 | ? 'https://placedog.net/300/300' 17 | : 'http://placekitten.com/300/300' 18 | } 19 | }, 20 | User: { 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /api/src/schema.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('apollo-server') 2 | 3 | /** 4 | * Type Definitions for our Schema using the SDL. 5 | */ 6 | const typeDefs = gql` 7 | 8 | 9 | `; 10 | 11 | module.exports = typeDefs 12 | -------------------------------------------------------------------------------- /api/src/server.js: -------------------------------------------------------------------------------- 1 | const { ApolloServer } = require('apollo-server') 2 | const typeDefs = require('./schema') 3 | const resolvers = require('./resolvers') 4 | const {models, db} = require('./db') 5 | 6 | const server = new ApolloServer() 7 | 8 | server.listen().then(({ url }) => { 9 | console.log(`🚀 Server ready at ${url}`); 10 | }) 11 | -------------------------------------------------------------------------------- /api/tests/mutations.test.js: -------------------------------------------------------------------------------- 1 | describe('mutations', () => { 2 | test('hello', () => { 3 | expect(1).toBe(1) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /api/tests/queries.test.js: -------------------------------------------------------------------------------- 1 | describe('queries', () => { 2 | test('hello', () => { 3 | expect(1).toBe(1) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /api/tests/resolvers.test.js: -------------------------------------------------------------------------------- 1 | describe('resolvers', () => { 2 | test('hello', () => { 3 | expect(1).toBe(1) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins": ["emotion", "transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React starter app 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/src/client.js: -------------------------------------------------------------------------------- 1 | import { ApolloClient } from 'apollo-client' 2 | import { InMemoryCache } from 'apollo-cache-inmemory' 3 | import { HttpLink } from 'apollo-link-http' 4 | import gql from 'graphql-tag' 5 | 6 | 7 | const client = new ApolloClient() 8 | 9 | export default client 10 | -------------------------------------------------------------------------------- /client/src/components/App.js: -------------------------------------------------------------------------------- 1 | import { Switch, Route } from 'react-router-dom' 2 | import React, {Fragment} from 'react' 3 | import Header from './Header' 4 | import Pets from '../pages/Pets' 5 | 6 | const App = () => ( 7 | 8 |
9 |
10 | 11 | 12 | 13 |
14 | 15 | ) 16 | 17 | export default App 18 | -------------------------------------------------------------------------------- /client/src/components/Error.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrontendMasters/fullstack-graphql/d687926802b19ab25bc58c700c74c5c56b68192d/client/src/components/Error.js -------------------------------------------------------------------------------- /client/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { withRouter } from 'react-router' 4 | 5 | const Header = () => 6 |
7 |
8 |
9 | 10 | Home 11 | 12 |
13 |
14 |
15 | 16 | export default withRouter(Header) 17 | -------------------------------------------------------------------------------- /client/src/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ClipLoader from 'react-spinners/ClipLoader' 3 | 4 | const Loader = () => 5 |
6 | 12 |
13 | 14 | export default Loader 15 | -------------------------------------------------------------------------------- /client/src/components/NewPet.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import Select from 'react-select' 3 | 4 | const options = [ 5 | { value: 'CAT', label: 'Cat' }, 6 | { value: 'DOG', label: 'Dog' } 7 | ] 8 | 9 | export default function NewPet({onSubmit, onCancel}) { 10 | const [type, setType] = useState('DOG') 11 | const [name, setName] = useState('') 12 | 13 | const activeOption = options.find(o => o.value === type) 14 | 15 | const submit = e => { 16 | e.preventDefault() 17 | onSubmit({name, type}) 18 | } 19 | 20 | const cancel = e => { 21 | e.preventDefault() 22 | onCancel() 23 | } 24 | 25 | return ( 26 |
27 |

New Pet

28 |
29 |
30 | setName(e.target.value)} 43 | required 44 | /> 45 | cancel 46 | 47 |
48 |
49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /client/src/components/PetBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const PetBox = ({pet}) => ( 4 |
5 |
6 | 7 |
8 |
{pet.name}
9 |
{pet.type}
10 |
11 | ) 12 | 13 | export default PetBox 14 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @import 'flexboxgrid'; 2 | 3 | :root { 4 | --bgColor: #EFEFEF; 5 | --primaryColor: #3454D1; 6 | --secondaryColor: #34D1BF; 7 | --textColor: #070707; 8 | --errorColor: #D1345B; 9 | --lightTextColor: #fff; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | font-family: 'Helvetica Neue', 'Segoe UI', Roboto, -apple-system, system-ui, BlinkMacSystemFont, Arial, sans-serif; 16 | background: var(--bgColor); 17 | } 18 | 19 | body * { 20 | box-sizing: border-box; 21 | } 22 | 23 | html { 24 | font-size: calc(0.6em + 1vw); 25 | color: var(--textColor); 26 | } 27 | 28 | .page { 29 | padding: 3em 6em; 30 | } 31 | 32 | .row { 33 | margin: 0px; 34 | } 35 | 36 | header { 37 | height: 65px; 38 | width: 100vw; 39 | background: var(--primaryColor); 40 | padding: 10px 30px; 41 | } 42 | 43 | .link, a { 44 | text-decoration: none; 45 | color: var(--secondaryColor); 46 | } 47 | 48 | .full-page-loader { 49 | position: absolute; 50 | top: 0; 51 | left: 0; 52 | background: rgba(255,255,255, .8); 53 | width: 100vw; 54 | height: 100vh; 55 | display: flex; 56 | justify-content: center; 57 | align-items: center; 58 | } 59 | 60 | .full-page { 61 | height: 100vh; 62 | width: 100vw; 63 | } 64 | 65 | .full-height { 66 | height: 100%; 67 | } 68 | 69 | .form-box { 70 | background: white; 71 | padding: 1.5em; 72 | border-radius: 3px; 73 | } 74 | 75 | .box { 76 | background: white; 77 | padding: 1.5em; 78 | border-radius: 4px; 79 | border: 1px solid lightgrey; 80 | } 81 | 82 | .input { 83 | padding: .5em; 84 | outline: none; 85 | border: 1px solid lightgrey; 86 | border-radius: 3px; 87 | height: 1.6rem; 88 | font-size: 1rem; 89 | width: 100%; 90 | margin: 1em 0; 91 | } 92 | 93 | .input:focus { 94 | outline: none; 95 | } 96 | 97 | button,.button { 98 | outline: none; 99 | border: none; 100 | color: var(--lightTextColor); 101 | font-size: .8rem; 102 | text-transform: uppercase; 103 | padding: .5rem; 104 | background: var(--primaryColor); 105 | border-radius: 3px; 106 | cursor: pointer; 107 | } 108 | 109 | button.error, .button.error { 110 | background: var(--errorColor); 111 | } 112 | 113 | .col { 114 | padding-top: .5rem; 115 | padding-bottom: .5rem 116 | } 117 | 118 | figure { 119 | width: 200px; 120 | height: 200px; 121 | } 122 | 123 | figure img { 124 | width: 100%; 125 | } 126 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { BrowserRouter } from 'react-router-dom' 4 | import { ApolloProvider } from '@apollo/react-hooks' 5 | import App from './components/App' 6 | import client from './client' 7 | import './index.css' 8 | 9 | const Root = () => ( 10 | 11 | 12 | 13 | ) 14 | 15 | ReactDOM.render(, document.getElementById('app')) 16 | 17 | if (module.hot) { 18 | module.hot.accept() 19 | } 20 | -------------------------------------------------------------------------------- /client/src/pages/Pets.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import gql from 'graphql-tag' 3 | import PetBox from '../components/PetBox' 4 | import NewPet from '../components/NewPet' 5 | import { useQuery, useMutation } from '@apollo/react-hooks' 6 | import Loader from '../components/Loader' 7 | 8 | export default function Pets () { 9 | const [modal, setModal] = useState(false) 10 | 11 | const onSubmit = input => { 12 | setModal(false) 13 | } 14 | 15 | const petsList = pets.data.pets.map(pet => ( 16 |
17 |
18 | 19 |
20 |
21 | )) 22 | 23 | if (modal) { 24 | return ( 25 |
26 |
27 | setModal(false)}/> 28 |
29 |
30 | ) 31 | } 32 | 33 | return ( 34 |
35 |
36 |
37 |
38 |

Pets

39 |
40 | 41 |
42 | 43 |
44 |
45 |
46 |
47 |
48 | {petsList} 49 |
50 |
51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/react-hooks": "^3.1.2", 7 | "apollo-cache-inmemory": "^1.6.3", 8 | "apollo-client": "^2.6.4", 9 | "apollo-link-http": "^1.5.16", 10 | "apollo-server": "^2.9.6", 11 | "babel-plugin-emotion": "^10.0.21", 12 | "flexboxgrid": "^6.3.1", 13 | "graphql": "^14.5.8", 14 | "graphql-tag": "^2.10.1", 15 | "json5": "^2.1.0", 16 | "lowdb": "^1.0.0", 17 | "nanoid": "^2.1.1", 18 | "react": "^16.10.1", 19 | "react-dom": "^16.10.1", 20 | "react-router": "^5.1.2", 21 | "react-router-dom": "^5.1.2", 22 | "react-select": "^3.0.8", 23 | "react-spinners": "^0.6.1" 24 | }, 25 | "scripts": { 26 | "server": "node api/src/server.js", 27 | "app": "parcel client/index.html --out-dir client/dist", 28 | "test-server": "jest api/tests" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "@playlyfe/gql": "^2.6.2", 44 | "babel-core": "^6.26.3", 45 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 46 | "babel-preset-env": "^1.7.0", 47 | "babel-preset-react": "^6.24.1", 48 | "jest": "^24.9.0", 49 | "parcel": "1.12.3" 50 | } 51 | } 52 | --------------------------------------------------------------------------------