├── .gitignore ├── client.html ├── package.json ├── server.js ├── queries.gql ├── resolvers.js ├── data.js ├── typeDefs.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | GraphQL Fantasy API Client 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-fantasy-api", 3 | "dependencies": { 4 | "@graphql-tools/schema": "^8.1.2", 5 | "express": "^4.17.1", 6 | "express-graphql": "^0.12.0", 7 | "graphql": "^15.5.1", 8 | "nodemon": "^2.0.12" 9 | }, 10 | "main": "server.js", 11 | "scripts": { 12 | "dev": "nodemon server.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const graphQL = require('graphql') 3 | const { graphqlHTTP } = require('express-graphql') 4 | const { makeExecutableSchema } = require('@graphql-tools/schema') 5 | 6 | const data = require('./data.js') 7 | const { typeDefs } = require('./typeDefs.js') 8 | const { resolvers } = require('./resolvers.js') 9 | 10 | const executableSchema = makeExecutableSchema({ 11 | typeDefs, 12 | resolvers, 13 | }) 14 | 15 | const app = express() 16 | app.use(express.json()) 17 | app.use(express.urlencoded({ extended: true })) 18 | app.use( 19 | '/graphql', 20 | graphqlHTTP({ 21 | schema: executableSchema, 22 | context: data, 23 | graphiql: true, 24 | }) 25 | ) 26 | 27 | app.listen(4000, () => { 28 | console.log('Running a GraphQL API server at http://localhost:4000/graphql') 29 | }) 30 | -------------------------------------------------------------------------------- /queries.gql: -------------------------------------------------------------------------------- 1 | query FetchWizards { 2 | wizards { 3 | name 4 | level 5 | race 6 | job 7 | spells { 8 | name 9 | attack 10 | range 11 | } 12 | } 13 | } 14 | 15 | query FetchSpecificWizardQuery { 16 | wizard(id: "1") { 17 | name 18 | } 19 | } 20 | 21 | # Pass data in Query Variables 22 | query FetchSomeWizard($wizardId: ID!) { 23 | wizard(id: $wizardId) { 24 | id 25 | name 26 | } 27 | } 28 | 29 | query FetchAllCharacters { 30 | characters { 31 | ... on Fighter { 32 | id 33 | name 34 | } 35 | ... on Wizard { 36 | id 37 | name 38 | } 39 | } 40 | } 41 | 42 | mutation AddCharacter($input: CharacterInput!) { 43 | addCharacter(input: $input) { 44 | __typename 45 | ... on Fighter { 46 | id 47 | name 48 | race 49 | weapon { 50 | name 51 | } 52 | } 53 | ... on Wizard { 54 | id 55 | name 56 | race 57 | spells { 58 | name 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /resolvers.js: -------------------------------------------------------------------------------- 1 | const resolvers = { 2 | Query: { 3 | fighter: (obj, { id }, context) => context.fighters.find(fighter => fighter.id === id), 4 | fighters: (obj, args, context) => context.fighters, 5 | wizard: (obj, { id }, context) => context.wizards.find(wizard => wizard.id === id), 6 | wizards: (obj, args, context) => context.wizards, 7 | characters: (obj, args, context) => context.characters, 8 | }, 9 | 10 | Mutation: { 11 | addCharacter: (obj, { input }, context) => { 12 | const newCharacter = { 13 | id: Math.floor(Math.random() * 100).toString(), 14 | ...input, 15 | level: 1, 16 | } 17 | 18 | if (input.job === 'FIGHTER') { 19 | context.fighters.push(newCharacter) 20 | } 21 | 22 | if (input.job === 'WIZARD') { 23 | context.wizards.push({ ...newCharacter, spells: [] }) 24 | } 25 | 26 | return newCharacter 27 | }, 28 | }, 29 | 30 | CharacterType: { 31 | __resolveType: (obj, context, info) => { 32 | if (obj.job === 'FIGHTER') { 33 | return 'Fighter' 34 | } 35 | 36 | if (obj.job === 'WIZARD') { 37 | return 'Wizard' 38 | } 39 | 40 | return null 41 | }, 42 | }, 43 | } 44 | 45 | module.exports = { resolvers } 46 | -------------------------------------------------------------------------------- /data.js: -------------------------------------------------------------------------------- 1 | // Enums 2 | const races = { 3 | HUMAN: 'HUMAN', 4 | DWARF: 'DWARF', 5 | ELF: 'ELF', 6 | } 7 | 8 | const jobs = { 9 | FIGHTER: 'FIGHTER', 10 | WIZARD: 'WIZARD', 11 | } 12 | 13 | // Equipment and Abilities 14 | const weapons = [ 15 | { name: 'Sword', attack: 3, range: 1 }, 16 | { name: 'Bow', attack: 2, range: 3 }, 17 | { name: 'Axe', attack: 4, range: 1 }, 18 | ] 19 | 20 | const spells = [ 21 | { name: 'Lightning Bolt', attack: 2, range: 2 }, 22 | { name: 'Ice Storm', attack: 2, range: 2, effect: 'Frozen' }, 23 | { name: 'Fireball', attack: 2, range: 2 }, 24 | ] 25 | 26 | // Individual heroes 27 | const fighters = [ 28 | { 29 | id: '1', 30 | name: 'Arthur', 31 | level: 1, 32 | weapon: weapons.find(weapon => weapon.name === 'Sword'), 33 | race: races.HUMAN, 34 | job: jobs.FIGHTER, 35 | __typename: 'Fighter', 36 | }, 37 | { 38 | id: '2', 39 | name: 'Gimli', 40 | level: 1, 41 | weapon: weapons.find(weapon => weapon.name === 'Axe'), 42 | race: races.DWARF, 43 | job: jobs.FIGHTER, 44 | __typename: 'Fighter', 45 | }, 46 | ] 47 | 48 | const wizards = [ 49 | { 50 | id: '1', 51 | name: 'Merlin', 52 | level: 1, 53 | spells, 54 | race: races.HUMAN, 55 | job: jobs.WIZARD, 56 | __typename: 'Wizard', 57 | }, 58 | { 59 | id: '2', 60 | name: 'Gandalf', 61 | level: 1, 62 | spells, 63 | race: races.HUMAN, 64 | job: jobs.WIZARD, 65 | __typename: 'Wizard', 66 | }, 67 | ] 68 | 69 | const characters = [...fighters, ...wizards] 70 | 71 | module.exports = { 72 | races, 73 | jobs, 74 | weapons, 75 | spells, 76 | fighters, 77 | wizards, 78 | characters, 79 | } 80 | -------------------------------------------------------------------------------- /typeDefs.js: -------------------------------------------------------------------------------- 1 | const typeDefs = ` 2 | "The job class of the character." 3 | enum Job { 4 | FIGHTER 5 | WIZARD 6 | } 7 | 8 | "The species or ancestry of the character." 9 | enum Race { 10 | HUMAN 11 | ELF 12 | DWARF 13 | } 14 | 15 | "A valiant weapon wielded by a fighter." 16 | type Weapon { 17 | name: String! 18 | attack: Int 19 | range: Int 20 | } 21 | 22 | "A powerful spell that a wizard can read from a scroll." 23 | type Spell { 24 | name: String! 25 | attack: Int 26 | range: Int 27 | effect: String 28 | } 29 | 30 | "A hero on a quest." 31 | interface Character { 32 | id: ID! 33 | name: String! 34 | level: Int! 35 | race: Race 36 | job: Job 37 | } 38 | 39 | "A hero with direct combat ability and strength." 40 | type Fighter implements Character { 41 | id: ID! 42 | name: String! 43 | level: Int! 44 | race: Race 45 | job: Job! 46 | weapon: Weapon 47 | } 48 | 49 | "A hero with a variety of magical powers." 50 | type Wizard implements Character { 51 | id: ID! 52 | name: String! 53 | level: Int! 54 | race: Race 55 | job: Job! 56 | spells: [Spell] 57 | } 58 | 59 | input CharacterInput { 60 | name: String! 61 | race: Race 62 | job: Job 63 | } 64 | 65 | union CharacterType = Wizard | Fighter 66 | 67 | type Mutation { 68 | addCharacter(input: CharacterInput): CharacterType 69 | } 70 | 71 | type Query { 72 | fighters: [Fighter] 73 | fighter(id: ID!): Fighter 74 | wizards: [Wizard] 75 | wizard(id: ID!): Wizard 76 | characters: [CharacterType] 77 | } 78 | ` 79 | 80 | module.exports = { typeDefs } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Outline 2 | 3 | GraphQL Fantasy API 4 | 5 | ## An Overview of GraphQL 6 | 7 | - What is GraphQL? 8 | - GraphQL vs. REST 9 | - GraphQL server and client 10 | - Important Concepts and Terminology 11 | - Schema 12 | - Resolvers 13 | - Types 14 | - Queries 15 | - Mutations 16 | 17 | ## Setting up a GraphQL API Server and Client (Node) 18 | 19 | Tutorial walkthrough. 20 | 21 | - Server 22 | - Create a simple back end (Node/Express) that returns a single `/graphql` endpoint 23 | - Using "in memory" data store to avoid overhead of databases 24 | - Using Node 16/module type to avoid overhead of webpack 25 | - Client 26 | - Create a simple front end (vanilla JS? React?) that queries the `/graphql` endpoint 27 | - GraphiQL 28 | -Using GraphiQL for local development testing 29 | 30 | ## Creating a GraphQL Schema 31 | 32 | Using Fantasy/RPG data for example schema. Build it into the graphQL server above. 33 | 34 | - Type System 35 | - GraphQL Object Type (`type`, fields (properties)) 36 | - Scalar Type (aka primitives, `String`, `Int`, `Boolean`, `Float`) 37 | - Enumeration (`enum`) 38 | - Interface/Union Type (`interface`, `|` (union)) 39 | - Type Modifiers (`!` (non-nullable) and `[]` (list)) 40 | - Entrypoint (Operation) Types (`Query` and `Mutation`) 41 | - Comments/description (`""`) 42 | 43 | ## Using GraphQL Queries, Mutations, and Resolvers 44 | 45 | - Queries (read) 46 | - Simple query (no name) 47 | - Operation names (`query`) 48 | - Aliases 49 | - Arguments 50 | - hard-coded 51 | - query parameter 52 | - directives 53 | - Fragments (`fragment` and `...`) 54 | - Mutations (write) 55 | - Input Types (`input`) 56 | - Resolvers 57 | - Resolver functions 58 | - `resolveType` 59 | - Context 60 | 61 | ## Additional Information 62 | 63 | - Pagination: Connections Pattern - Edges and Nodes 64 | - Introspection (`__schema`, `__type`, `name`, `kind`, `ofType`) 65 | --------------------------------------------------------------------------------