{"id":13630477,"url":"https://github.com/nathanchapman/graphql-music","last_synced_at":"2025-08-21T01:32:23.940Z","repository":{"id":34565029,"uuid":"152138722","full_name":"nathanchapman/graphql-music","owner":"nathanchapman","description":"🎸 A workshop in building a GraphQL API","archived":false,"fork":false,"pushed_at":"2025-08-06T20:56:21.000Z","size":169,"stargazers_count":48,"open_issues_count":1,"forks_count":4,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-08-17T03:40:02.046Z","etag":null,"topics":["api","bandsintown","graphql","graphql-api","graphql-workshop","itunes","music","workshop"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nathanchapman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-10-08T20:00:59.000Z","updated_at":"2025-06-08T10:16:24.000Z","dependencies_parsed_at":"2024-08-01T22:52:21.371Z","dependency_job_id":null,"html_url":"https://github.com/nathanchapman/graphql-music","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/nathanchapman/graphql-music","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanchapman%2Fgraphql-music","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanchapman%2Fgraphql-music/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanchapman%2Fgraphql-music/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanchapman%2Fgraphql-music/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nathanchapman","download_url":"https://codeload.github.com/nathanchapman/graphql-music/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanchapman%2Fgraphql-music/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271415043,"owners_count":24755629,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-20T02:00:09.606Z","response_time":69,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","bandsintown","graphql","graphql-api","graphql-workshop","itunes","music","workshop"],"created_at":"2024-08-01T22:01:44.428Z","updated_at":"2025-08-21T01:32:23.699Z","avatar_url":"https://github.com/nathanchapman.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"# 🎸 graphql-music\n\nFor this introductory GraphQL workshop, we'll be building an API with music data to support clients that display information about artists, songs, lyrics, tabs (sheet music), and concerts 🎵\n\nThis workshop assumes you already have a basic knowledge of [what GraphQL is](https://www.howtographql.com/basics/0-introduction/) and [how to write code in Node.js](https://codeburst.io/the-only-nodejs-introduction-youll-ever-need-d969a47ef219).\n\nThis workshop typically takes about 2 to 2.5 hours to complete. I've broken it down into sections to make it easier to take breaks and jump back in whenever you're ready. To start at the beginning of any given section, just `git checkout` the branch with that name (i.e. `part1`, `part2`, etc.)\n\n## Contents\n\n### Setup\n\n\u003e Starting branch: [master](https://github.com/nathanchapman/graphql-music/tree/master)\n\n* [Setup](#setup)\n* [Organize](#organize)\n\n### Part 1\n\n\u003e Starting branch: [part1](https://github.com/nathanchapman/graphql-music/tree/part1)\n\n* [Creating your first Query](#creating-your-first-query)\n* [Creating your first Resolver](#creating-your-first-resolver)\n* [Let's get some Context](#lets-get-some-context)\n* [Creating your first Connector](#creating-your-first-connector)\n\n### Part 2\n\n\u003e Starting branch: [part2](https://github.com/nathanchapman/graphql-music/tree/part2)\n\n* [Song Data](#song-data)\n* [Limits](#limits)\n* [Graph Relationships](#graph-relationships)\n* [Lyrics and Tabs](#lyrics-and-tabs)\n\n### Part 3\n\n\u003e Starting branch: [part3](https://github.com/nathanchapman/graphql-music/tree/part3)\n\n* [Events](#events)\n* [Weather](#weather)\n\n### Part 4\n\n\u003e Starting branch: [part4](https://github.com/nathanchapman/graphql-music/tree/part4)\n\n* [More Graph Relationships](#more-graph-relationships)\n* [N+1 Queries](#n1-queries)\n* [DataLoader (Batching \u0026 Caching)](#dataloader-batching--caching)\n\n### Conclusion\n\n* [Conclusion](#%F0%9F%9A%80-conclusion)\n\n## Setup\n\nClone the project \u0026 `cd` into it\n\n```bash\n$ git clone git@github.com:nathanchapman/graphql-music.git\n$ cd graphql-music\n```\n\nInstall the dev dependencies\n\n```bash\n$ npm install\n```\n\nInstall [apollo-server](https://github.com/apollographql/apollo-server), [graphql-js](https://github.com/graphql/graphql-js), and [graphql-import](https://github.com/prisma/graphql-import) 🚀\n\n```bash\n$ npm install apollo-server graphql graphql-import\n```\n\nTake a look at the boilerplate code in `src/index.js`\n\n```js\nconst { ApolloServer, gql } = require('apollo-server');\n\nconst typeDefs = gql`\n  type Query {\n    greet(name: String): String!\n  }\n`;\n\nconst resolvers = {\n  Query: {\n    greet: (_, { name }) =\u003e `Hello ${name || 'World'}`,\n  },\n};\n\nconst server = new ApolloServer({\n  typeDefs,\n  resolvers,\n});\n\nserver.listen().then(({ url }) =\u003e {\n  console.log(`🚀 Server ready at ${url}`);\n});\n```\n\nAt this point, you should be able to run `npm start` to start the server. Your server will automatically restart each time we make changes. Navigate to \u003chttp://localhost:4000\u003e to see the demo server's Playground. [GraphQL Playground](https://www.apollographql.com/docs/apollo-server/features/graphql-playground.html) is a graphical, interactive, in-browser GraphQL IDE where you can explore the schema, craft queries, and view performance information like tracing.\n\nAt any point during this workshop, you can view the current schema by clicking the `SCHEMA` or `DOCS` buttons on the right side of the Playground. The development server will restart when you make changes to files in the project and Playground will automatically pick those up, so there's no need to refresh the page.\n\nWe can test our demo server by sending our first query in the Playground.\n\n```graphql\n{\n  greet\n}\n```\n\nThe response from a GraphQL server will be [JSON](https://www.w3schools.com/whatis/whatis_json.asp) in the same shape as the query you sent.\n\n```json\n{\n  \"data\": {\n    \"greet\": \"Hello World\"\n  }\n}\n```\n\n## Organize\n\nLet's organize things a little better!\n\nGo ahead and delete the example code for `typeDefs` and `resolvers` from `src/index.js`.\n\nCreate a folder `src/resolvers` and add an `index.js` to it.\n\nCreate another folder `src/schema` and add a file named `schema.graphql` to it.\n\nNow we need to import these into our `src/index.js`.\n\nYour `src/index.js` should look like this:\n\n```js\nconst { ApolloServer } = require('apollo-server');\nconst { importSchema } = require('graphql-import');\nconst resolvers = require('./resolvers');\n\nconst typeDefs = importSchema('src/schema/schema.graphql');\n\nconst server = new ApolloServer({\n  typeDefs,\n  resolvers,\n});\n\nserver.listen().then(({ url }) =\u003e {\n  console.log(`🚀 Server ready at ${url}`);\n});\n```\n\nAt this point, your changes should be in line with the starting branch for [part1](https://github.com/nathanchapman/graphql-music/tree/part1).\n\n## Creating your first Query\n\nWe know our clients will need information about `artist`s. Let's define what an artist is by adding the `Artist` type in a new file `src/schema/artist.graphql`.\n\n```graphql\ntype Artist {\n  id: ID!\n  name: String!\n  url: String\n  genre: String\n}\n```\n\n***Note:*** These fields should be determined by **both** the needs of the clients and capabilities of our backend APIs.\n\nNow let's add our first Query to `src/schema/schema.graphql`\n\n```graphql\n# import Artist from 'artist.graphql'\n\ntype Query {\n  artists(name: String!): [Artist]!\n}\n```\n\nThis query will allow our clients to search for artists and get an array of results!\n\nNotice the `import` statement in our `schema.graphql` and how we're using `importSchema` in our `src/index.js` file? Both of those come from the [graphql-import](https://github.com/prisma/graphql-import) module we installed earlier.\n\nThere are [several ways to represent a GraphQL schema](https://blog.apollographql.com/three-ways-to-represent-your-graphql-schema-a41f4175100d), including: using the [GraphQL.js](https://github.com/graphql/graphql-js#using-graphqljs) `GraphQLSchema` and `GraphQLObjectType` classes or [GraphQL Schema Definition Language](https://www.prisma.io/blog/graphql-sdl-schema-definition-language-6755bcb9ce51) (SDL). We'll be using GraphQL SDL in this workshop because it's the most popular and arguably easier to read and understand.\n\nFurther, you can represent GraphQL SDL in a number of ways, including: [strings](https://www.apollographql.com/docs/graphql-tools/generate-schema#example), [graphql-tag](https://github.com/apollographql/graphql-tag#gql) (gql), and directly in `.graphql` or `.gql` files. Any of these approaches work just fine, but we'll be using `.graphql` files to keep things simple and reduce the amount of boilerplate code.\n\n[graphql-import](https://github.com/prisma/graphql-import) gives us the ability to import our schema into JavaScript files using `importSchema` as well as break up our schema into different files by letting us import `.graphql` files into other `.graphql` files. Pretty cool!\n\nNow we have our type definitions for what an `Artist` is and how to `Query` for one. Awesome! But how do we actually fetch and return data?\n\n## Creating your first Resolver\n\n`Resolvers` are functions that are executed by our server to resolve the data for our schema. The object we create containing these functions will have the same shape as our schema. We can define a resolver for any field on any type, but often times we're able to rely on the [default resolver](https://www.apollographql.com/docs/graphql-tools/resolvers/#default-resolver) for trivial resolutions like returning a named property on an object.\n\nAdd a `Query.artists` resolver to `src/resolvers/index.js`\n\n```js\nconst resolvers = {\n  Query: {\n    artists: (_, { name }) =\u003e [{ name }],\n  },\n};\n\nmodule.exports = resolvers;\n```\n\nOpen the Playground at \u003chttp://localhost:4000\u003e and send a query for `artists`\n\n```graphql\n{\n  artists(name: \"Fake\") {\n    name\n  }\n}\n```\n\nYou'll receive fake data because we're just mocking an array with one object as the return value of the resolver, but now we have something executing!\n\n*Notice* that if you ask for any non-nullable fields (denoted with a `!` in the schema) like `id`, you'll get an error `Cannot return null for non-nullable field Artist.id`. This is because we aren't returning a value for `id` from our resolver, only the `name`. Asking for normal, nullable fields like `url` and `genre` won't cause an error because they aren't guaranteed by our GraphQL server based on the type definition of `Artist` in the schema. For those nullable fields, you'll just receive `null` when no value is returned from the resolver.\n\n## Let's get some Context\n\nResolvers take in 4 parameters: `root`, `args`, `context`, and `info`, respectively.\n\n* `root` the value of the previous execution level (more on `execution levels` later)\n* `args` any arguments passed directly into the given field\n* `context` an object containing any data that should be made available to all resolvers (think logging functions, session information, data sources used to fetch information, etc.)\n* `info` an object containing information about the query such as the selection set, the AST of the query, parent information, etc. This parameter isn't used as often, and I'd consider it as intended for more advanced cases.\n\n## Creating your first Connector\n\nMost GraphQL services follow some sort of `connector` pattern for data access. The idea here is to have a layer on top of a database/backend driver that has GraphQL-specific error handling, logging, batching, and caching. We'll touch more on these topics later. For now, let's just think of it as our sources for fetching data.\n\nYou guessed it! The connector will go on the `context` object passed into all resolvers.\n\nLet's create a new folder `src/connectors` with an `index.js`\n\n```js\nconst createConnectors = () =\u003e ({});\n\nmodule.exports = createConnectors;\n```\n\nIn our `src/index.js`, let's import that file and update our server to include a new `context` object\n\n```js\n...\nconst createConnectors = require('./connectors');\n\nconst typeDefs = importSchema('src/schema/schema.graphql');\nconst context = { connectors: createConnectors() };\n\nconst server = new ApolloServer({\n  typeDefs,\n  resolvers,\n  context,\n});\n...\n```\n\nLet's add a new file, `connectors/iTunes.js`\n\n```js\nclass iTunes {}\n\nmodule.exports = iTunes;\n```\n\nand import it into `connectors/index.js`\n\n```js\nconst iTunes = require('./iTunes');\n\nconst createConnectors = () =\u003e ({\n  iTunes: new iTunes(),\n});\n\nmodule.exports = createConnectors;\n```\n\nWe'll need to make an [HTTP request](https://www.codecademy.com/articles/http-requests) to the iTunes API in our `iTunes` connector so we'll be using [got](https://github.com/sindresorhus/got), a simplified HTTP request library with support for [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).\n\nLet's kill our server with `ctrl+c`, install [got](https://github.com/sindresorhus/got), and start the server back up.\n\n```bash\n$ npm install got\n$ npm start\n```\n\nNow we can make asynchronous HTTP requests!\n\nAt the top of `connectors/iTunes.js`, let's `require` the new dependency\n\n```js\nconst { get } = require('got');\n```\n\nAnd let's add our first method inside the `iTunes` class\n\n```js\nasync artists({ name }) {\n  const options = {\n    query: {\n      term: name,\n      country: 'us',\n      entity: 'allArtist',\n    },\n    json: true,\n  };\n\n  const { body } = await get('https://itunes.apple.com/search', options);\n  const { results } = body;\n  return results.map(artist =\u003e ({\n    name: artist.artistName,\n    url: artist.artistLinkUrl,\n    id: artist.artistId,\n    genre: artist.primaryGenreName,\n  }));\n}\n```\n\n*Notice* that once we get the results, we're remapping the iTunes API results into objects that match our GraphQL type for `Artist`.\n\nNow we can go back to `resolvers/index.js` and consume this connector from our `context`\n\n```js\nQuery: {\n  artists: (_, args, ctx) =\u003e ctx.connectors.iTunes.artists(args),\n},\n```\n\nAnd that's it!\n\nYou can open the [Playground](http://localhost:4000) again and send a query for `artists`:\n\n```graphql\n{\n  artists(name: \"The Beatles\") {\n    id\n    name\n    url\n    genre\n  }\n}\n```\n\nIt works! 😎\n\nAt this point, your changes should be in line with the starting branch for [part2](https://github.com/nathanchapman/graphql-music/tree/part2).\n\n## Song Data\n\nCreate a `Song` type in a new file `src/schema/song.graphql`.\n\n```graphql\ntype Song {\n  id: ID!\n  name: String!\n  artistName: String\n  album: String\n  url: String\n}\n```\n\nand add a new `Query` for `songs` in `src/schema/schema.graphql`\n\n```graphql\n# import Artist from 'artist.graphql'\n# import Song from 'song.graphql'\n\ntype Query {\n  artists(name: String!): [Artist]!\n  songs(name: String!): [Song]!\n}\n```\n\nLet's add another method to the `iTunes` connector\n\n```js\nasync songs({ name }) {\n  const options = {\n    query: {\n      term: name,\n      country: 'us',\n      entity: 'song',\n    },\n    json: true,\n  };\n\n  const { body } = await get('https://itunes.apple.com/search', options);\n  const { results } = body;\n  return results.map(song =\u003e ({\n    name: song.trackName,\n    artistName: song.artistName,\n    album: song.collectionName,\n    url: song.trackViewUrl,\n    id: song.trackId,\n  }));\n}\n```\n\n*Notice* we're remapping the results again since the iTunes API definition of a song isn't exactly the same as the `Song` type definition we're using in our GraphQL API.\n\nNow we just have to add a resolver for the `songs` query\n\n```js\nQuery: {\n  artists: (_, args, ctx) =\u003e ctx.connectors.iTunes.artists(args),\n  songs: (_, args, ctx) =\u003e ctx.connectors.iTunes.songs(args),\n},\n```\n\nOpen the [Playground](http://localhost:4000) again and send a query for `songs`\n\n```graphql\n{\n  songs(name: \"Abbey Road\") {\n    id\n    name\n    artistName\n    album\n    url\n  }\n}\n```\n\nWow.. there are a lot more results than our clients need to display! This large payload will have to be downloaded and parsed by the client whether they use all of the results or not. Let's fix that!\n\n## Limits\n\nLet's add some limiting to our queries so the clients can specify how many results they need.\n\nIn your `schema`, add limit query parameters with some reasonable defaults\n\n```graphql\ntype Query {\n  artists(name: String!, limit: Int = 5): [Artist]!\n  songs(name: String!, limit: Int = 10): [Song]!\n}\n```\n\nIn your `iTunes` connector, add `limit` in **both** the `artists` and `songs` method signatures and to their `options.qs` objects\n\n```js\nasync artists({ name, limit }) {\n  const options = {\n    query: {\n      term: name,\n      country: 'us',\n      entity: 'allArtist',\n      limit,\n    },\n    json: true,\n  };\n  ...\n}\n```\n\nNow the clients can specify a limit or rely on the defaults we set in our schema\n\n```graphql\n{\n  songs(name: \"Abbey Road\", limit: 1) {\n    id\n    name\n    artistName\n    album\n    url\n  }\n}\n```\n\n## Graph Relationships\n\nNow we can request information about artists and songs, but they're separate.\n\nOur clients would have to send queries like this for artist info and their songs:\n\n```graphql\n{\n  artists(name: \"The Beatles\", limit: 1) {\n    id\n    name\n    url\n    genre\n  }\n  songs(name: \"The Beatles\") {\n    id\n    name\n    artistName\n    album\n    url\n  }\n}\n```\n\nWe could improve this slightly by using `query variables`\n\n```graphql\nquery artistsWithSongs($name: String!) {\n  artists(name: $name, limit: 1) {\n    id\n    name\n    url\n    genre\n  }\n  songs(name: $name) {\n    id\n    name\n    artistName\n    album\n    url\n  }\n}\n```\n\n```json\n{\n  \"name\": \"The Beatles\"\n}\n```\n\nBut there's still no direct relationship between an `Artist` and their `songs`.\n\nShouldn't we be able to query for `songs` under an `artist` and vice versa?\n\nIn your `schema`, add a `songs` field under the `Artist` type\n\n```graphql\ntype Artist {\n  id: ID!\n  name: String!\n  url: String\n  genre: String\n  songs(limit: Int = 10): [Song]!\n}\n```\n\nand in your `resolvers` add a new type resolver object for `Artist` with a resolver for `songs`\n\n```js\nQuery: {\n  ...\n},\nArtist: {\n  songs: ({ name }, { limit }, ctx) =\u003e (\n    ctx.connectors.iTunes.songs({ name, limit })\n  ),\n},\n```\n\nOur `Query.artists` resolver doesn't return the necessary data for `songs`. That's okay! In the next `execution level`, the `Artist.songs` resolver is called on the `Artist` object to fetch this data.\n\nThis new field resolver is almost identical to the `Query.songs` resolver, but the `name` comes from the root object `Artist` (once it's resolved) instead of a field argument.\n\nNow our clients can send more concise queries for artist info and their songs\n\n```graphql\n{\n  artists(name: \"The Beatles\") {\n    id\n    name\n    url\n    genre\n    songs {\n      id\n      name\n      artistName\n      album\n      url\n    }\n  }\n}\n```\n\n## Lyrics and Tabs\n\nThere's a new feature coming out soon and the clients need to get data for lyrics and tabs (sheet music), but neither of those are supported by the iTunes API.\n\nGo ahead and add these fields to the `Song` type in your `schema`\n\n```graphql\ntype Song {\n  id: ID!\n  name: String!\n  artistName: String\n  album: String\n  url: String\n  lyrics: String\n  tabs: String\n}\n```\n\nAdd a new file `src/connectors/Lyrics.js`\n\n```js\nconst { get } = require('got');\n\nclass Lyrics {\n  async bySong({ name, artistName }) {\n    const options = {\n      json: true,\n    };\n\n    const url = `https://api.lyrics.ovh/v1/${artistName}/${name}`;\n    try {\n      const { body } = await get(url, options);\n      return body.lyrics;\n    } catch (error) {\n      return null;\n    }\n  }\n}\n\nmodule.exports = Lyrics;\n```\n\nLet's import it in `connectors/index.js`\n\n```js\n...\nconst Lyrics = require('./Lyrics');\n\nconst createConnectors = () =\u003e ({\n  ...\n  Lyrics: new Lyrics(),\n});\n\nmodule.exports = createConnectors;\n```\n\nand in your `resolvers`, add a new root type resolver object for `Song` with a field resolver for `lyrics`\n\n```js\nQuery: {\n  ...\n},\nArtist: {\n  ...\n},\nSong: {\n  lyrics: (song, _, ctx) =\u003e ctx.connectors.Lyrics.bySong(song),\n},\n```\n\nWe have lyrics! 🎤\n\nWhat about tabs?\n\n[Songterr](https://www.songsterr.com/) provides tabs and an API, but they also have direct URLs we can use for loading sheet music by artist name and song name. That's all our clients needed! In this case, we don't even need a connector or an API call.\n\nJust add a field resolver for `tabs` under the `Song` object\n\n```js\n...\nSong: {\n  lyrics: (song, _, ctx) =\u003e ctx.connectors.Lyrics.bySong(song),\n  tabs: ({ name, artistName }) =\u003e (\n    `http://www.songsterr.com/a/wa/bestMatchForQueryString?s=${name}\u0026a=${artistName}`\n  ),\n},\n```\n\nOpen the [Playground](http://localhost:4000) again and send a query for `songs` with lyrics and tabs\n\n```graphql\n{\n  songs(name: \"Here Comes The Sun\", limit: 1) {\n    id\n    name\n    artistName\n    album\n    url\n    lyrics\n    tabs\n  }\n}\n```\n\n🎼🎼🎼🎼\n\nAt this point, your changes should be in line with the starting branch for [part3](https://github.com/nathanchapman/graphql-music/tree/part3).\n\n## Events\n\nLet's add some `Event`-related types to our `schema` (think concerts, festivals, etc.)\n\n\u003e src/schema/event.graphql\n\n```graphql\n# import Ticket from 'ticket.graphql'\n# import Venue from 'venue.graphql'\n\ntype Event {\n  date: String!\n  time: String!\n  venue: Venue\n  tickets: Ticket\n  lineup: [String]\n}\n```\n\n\u003e src/schema/ticket.graphql\n\n```graphql\ntype Ticket {\n  status: String\n  url: String\n}\n```\n\n\u003e src/schema/venue.graphql\n\n```graphql\ntype Venue {\n  name: String\n  latitude: String\n  longitude: String\n  city: String\n  region: String\n  country: String\n}\n```\n\nand add an `events` field under the `Artist` type\n\n```graphql\n# import Event from 'event.graphql'\n\ntype Artist {\n  id: ID!\n  name: String!\n  url: String\n  genre: String\n  songs(limit: Int = 10): [Song]!\n  events(limit: Int = 10): [Event]\n}\n```\n\nWe'll need a connector for event data. For this we'll be using the [BandsInTown API](https://app.swaggerhub.com/apis/Bandsintown/PublicAPI/3.0.0#/).\n\nAdd a new file `connectors/BandsInTown.js`\n\n```js\nconst { get } = require('got');\n\nclass BandsInTown {\n  async events({ name, limit }) {\n    const options = {\n      json: true,\n    };\n\n    const url = `https://rest.bandsintown.com/artists/${name}/events?app_id=qfasdfasdf`;\n    const { body } = await get(url, options);\n    return body.slice(0, limit);\n  }\n}\n\nmodule.exports = BandsInTown;\n```\n\nand import it in `connectors/index.js`\n\n```js\n...\nconst BandsInTown = require('./BandsInTown');\n\nconst createConnectors = () =\u003e ({\n  ...\n  BandsInTown: new BandsInTown(),\n});\n\nmodule.exports = createConnectors;\n```\n\nAdd the `events` field resolver under the `Artist` root object\n\n```js\n...\nArtist: {\n  ...\n  events: ({ name }, { limit }, ctx) =\u003e (\n    ctx.connectors.BandsInTown.events({ name, limit })\n  ),\n},\n...\n```\n\nOpen the [Playground](http://localhost:4000) again and send a query for `artists` with events\n\n```graphql\n{\n  artists(name: \"Blink-182\", limit: 1) {\n    name\n    events {\n      date\n      time\n      venue {\n        name\n        latitude\n        longitude\n        city\n        region\n        country\n      }\n      tickets {\n        status\n        url\n      }\n      lineup\n    }\n  }\n}\n```\n\nUh oh! We got a `Cannot return null for non-nullable field Event.date` error from our GraphQL server.\n\nIt looks like both `date` and `time` are `null` even though we're guaranteeing them as non-nullable in our schema (denoted with a `!`)\n\nWhat caused the `null` values?\n\nThe BandsInTown API returned an object with a field named `datetime` instead of two different fields for `date` and `time`. Let's resolve the time and date from `datetime` using field resolvers on the `Event` type so our clients have the data they need.\n\n```js\n...\nEvent: {\n  time: event =\u003e new Date(event.datetime).toLocaleTimeString(),\n  date: event =\u003e new Date(event.datetime).toLocaleDateString(),\n},\n```\n\nThat's fixed, but `tickets` is `null`!\n\nThe response from `BandsInTown` has an `offers` array instead.\n\n```js\nEvent: {\n  ...\n  tickets: event =\u003e event.offers.find(offer =\u003e offer.type === 'Tickets'),\n},\n```\n\nNo more errors or `null` — awesome!\n\n## Weather\n\nNow let's add some `Weather` types to our `schema` so we can fetch the weather conditions on the day of an `Event`\n\n\u003e src/schema/weather.graphql\n\n```graphql\ntype Weather {\n  condition: String\n  temperature(unit: TemperatureUnit = F): Temperature\n}\n\ntype Temperature {\n  high: Int\n  low: Int\n  unit: TemperatureUnit\n}\n\nenum TemperatureUnit {\n  C\n  F\n}\n```\n\nUnder the `Event` type, add a `weather` field\n\n```graphql\n# import Ticket from 'ticket.graphql'\n# import Venue from 'venue.graphql'\n# import * from 'weather.graphql'\n\ntype Event {\n  date: String!\n  time: String!\n  venue: Venue\n  tickets: Ticket\n  lineup: [String]\n  weather: Weather\n}\n```\n\nThis workshop used to consume the Yahoo Weather API until the public version was removed in January 2019. It was here that I'd point out that Yahoo went as far as to create their own custom query language for interacting with their APIs called `yql`.\n\nYou'd pass the `yql` as part of the URL query string like so\n\n```js\nconst url = 'https://query.yahooapis.com/v1/public/yql?q='\n  .concat('select * from weather.forecast where woeid in ')\n  .concat(`(select woeid from geo.places(1) where text=\"${city}, ${region}\") `)\n  .concat(`and u='${unit.toLowerCase()}'\u0026format=json`)\n  .concat('\u0026env=store://datatables.org/alltableswithkeys'),\n```\n\n🤦‍ Not ideal...\n\nAny ideas for a technology that would greatly simplify their API? 💡\n\nAll we're asking for is forecast data for a `city` and `region`.\n\nWhat if we could send them a GraphQL query instead?\n\n```graphql\n{\n  forecast(city: $city, region: $region, unit: $unit) {\n    high\n    low\n    condition\n  }\n}\n```\n\nMaybe some day.. 🤞\n\nWe'll use [MetaWeather](https://www.metaweather.com/api/) instead. It's a bit easier to work with anyways.\n\nAdd a new connector `connectors/Weather.js`\n\n```js\nconst { get } = require('got');\n\nconst format = weather =\u003e ({\n  condition: weather.weather_state_name,\n  high: weather.max_temp,\n  low: weather.min_temp,\n});\n\nclass Weather {\n  async forecast({ datetime, venue }) {\n    const date = new Date(datetime);\n    const [year, month, day] = [\n      date.getFullYear(), date.getMonth() + 1, date.getDate(),\n    ];\n\n    const { latitude, longitude } = venue;\n\n    const location = {\n      query: {\n        lattlong: `${latitude},${longitude}`,\n      },\n      json: true,\n    };\n\n    const {\n      body: [{ woeid }], // use the first city woeid returned from the search\n    } = await get('https://www.metaweather.com/api/location/search/', location);\n\n    const options = { json: true };\n    const weather = y =\u003e m =\u003e d =\u003e `https://www.metaweather.com/api/location/${woeid}/${y}/${m}/${d}/`;\n\n    // Forecasts only work 5-10 days in the future\n    const { body: [forecasted] } = await get(\n      weather(year)(month)(day),\n      options,\n    );\n\n    if (forecasted) return format(forecasted);\n\n    // Fallback to last year's weather report\n    const { body: [historical] } = await get(\n      weather(year-1)(month)(day),\n      options,\n    );\n\n    if (historical) return format(historical);\n\n    throw new Error('Unable to retrieve weather data for event');\n  }\n}\n\nmodule.exports = Weather;\n```\n\nInitialize your `Weather` connector in `connectors/index.js` like you've done with the other connectors\n\n```js\n...\nconst Weather = require('./Weather');\n\nconst createConnectors = () =\u003e ({\n  ...\n  Weather: new Weather(),\n});\n\nmodule.exports = createConnectors;\n```\n\nAdd a field resolver for `weather` under the `Event` object\n\n```js\nEvent: {\n  ...\n  weather: ({ datetime, venue }, _, ctx) =\u003e (\n    ctx.connectors.Weather.forecast({ datetime, venue })\n  ),\n},\n```\n\n*Notice* we're using the `datetime` from the root object (`Event` returned by BandsInTown) even though we didn't publicly expose that field in our GraphQL API. GraphQL gives us the entire object returned from the previous execution level as `root` (the first argument). This is a great way to pass data from a root object to the next execution level without exposing the implementation details of your API!\n\nOpen the [Playground](http://localhost:4000) again and send a query for `artists` with `events` and `weather`\n\n```graphql\n{\n  artists(name: \"Blink-182\", limit: 1) {\n    name\n    events(limit: 1) {\n      weather {\n        temperature {\n          high\n          low\n          unit\n        }\n        condition\n      }\n      date\n      time\n      venue {\n        name\n        latitude\n        longitude\n        city\n        region\n        country\n      }\n      tickets {\n        status\n        url\n      }\n      lineup\n    }\n  }\n}\n```\n\n`temperature` is `null` 😧\n\nWe're returning the temperatures in our Weather connector as `Weather.high` and `Weather.low` (both in Celsius from the MetaWeather API). This doesn't quite line up with our schema! They should be under the `Weather.temperature` object and return the correct values for both Celsius and Farenheit. Let's fix it by adding a resolver for `Weather.temperature` that uses the `high` and `low` fields from the `Weather` object and handles conversion.\n\n```js\n...\nWeather: {\n  temperature: ({ high, low }, { unit }) =\u003e {\n    const fahrenheit = c =\u003e c * 9 / 5 + 32\n    const h = unit === 'C' ? high : fahrenheit(high);\n    const l = unit === 'C' ? low : fahrenheit(low);\n\n    return {\n      unit,\n      high: Math.round(h),\n      low: Math.round(l),\n    };\n  },\n},\n```\n\nTry the query again and make sure to prepare for the weather! ☔️⛅️😎\n\nOur clients can still get `null` for `weather`, but that's only if the MetaWeather API fails to return both the forecast and historical data. This is a lot less likely, but if it does fail, the clients will also get an error telling them about the issue.\n\n```json\n{\n  \"data\": {\n    \"artists\": [\n      {\n        \"name\": \"Blink-182\",\n        \"events\": [\n          {\n            \"weather\": null,\n            \"date\": \"2019-6-12\"\n          }\n        ]\n      }\n    ]\n  },\n  \"errors\": [\n    {\n      \"message\": \"Unable to retrieve weather data for event\",\n      \"locations\": [\n        {\n          \"line\": 5,\n          \"column\": 7\n        }\n      ],\n      \"path\": [\"artists\", 0, \"events\", 0, \"weather\"],\n      \"extensions\": {\n        \"code\": \"INTERNAL_SERVER_ERROR\",\n        \"exception\": {\n          \"stacktrace\": [\n            \"Error: Unable to retrieve weather data for event\"\n          ]\n        }\n      }\n    }\n  ],\n}\n```\n\nErrors are very useful! We can use errors to intelligently inform our clients about issues with provided inputs, API degredations, and many other types of issues. Remember to not expose sensitive information like unexpected errors, stacktraces, etc. in production! See [Apollo's Error handling guide](https://www.apollographql.com/docs/apollo-server/features/errors/) for more information.\n\nAt this point, your changes should be in line with the starting branch for [part4](https://github.com/nathanchapman/graphql-music/tree/part4).\n\n## More Graph Relationships\n\nOur clients want to be able to search for songs and get more artist information back than just `artistName`.\n\nRemember earlier when we set up a graph relationship between artists and their songs?\n\nWe should create a similar relationship between a `Song` and its `Artist`.\n\nIn your `schema`, add a field for `artist` under the `Song` type\n\n```graphql\ntype Song {\n  ...\n  artist: Artist\n}\n```\n\nWe'll need to modify our `songs` method in our `iTunes` connector to return the artist's ID\n\n```js\nreturn {\n  ...\n  artistId: song.artistId,\n};\n```\n\nLet's add another method to our `iTunes` connector to lookup an artist by ID\n\n```js\nasync artist({ id }) {\n  const options = {\n    query: { id },\n    json: true,\n  };\n\n  console.log(`looking up artist ${id}`);\n\n  const { body } = await get('https://itunes.apple.com/lookup', options);\n  const artist = body.results[0];\n\n  return {\n    name: artist.artistName,\n    url: artist.artistLinkUrl,\n    id: artist.artistId,\n    genre: artist.primaryGenreName,\n  };\n}\n```\n\nand add a field `resolver` for `artist` under the `Song` root object\n\n```js\nSong: {\n  ...\n  artist: ({ artistId }, _, ctx) =\u003e (\n    ctx.connectors.iTunes.artist({ id: artistId })\n  ),\n},\n```\n\nLastly, let's deprecate the old `artistName` field in our `schema` so that new clients won't know about that field. It will still work as expected for older clients that may still be requesting it and you should keep it around until you can confirm it's not being called anymore (think mobile apps that haven't been updated yet!)\n\n```graphql\ntype Song {\n  ...\n  artist: Artist\n  artistName: String @deprecated(reason: \"Use `artist.name`.\")\n  ...\n}\n```\n\nOpen the [Playground](http://localhost:4000) again and send a query for `songs` with artist details\n\n```graphql\n{\n  songs(name: \"Sun\") {\n    id\n    name\n    artistName\n    album\n    url\n    artist {\n      id\n      name\n      url\n      genre\n    }\n  }\n}\n```\n\nYou should notice a new deprecation warning on the `artistName` field.\n\nWe also have a working graph relationship between songs and artists — awesome!\n\n...But look at your `console`. Notice any duplicates?\n\nThis means we're fetching the same data multiple times from the iTunes API. This can overload your backend APIs and will cause your clients to spend additional time waiting for a response.\n\nLet's turn on `tracing` to keep an eye on the performance as we try to fix this.\n\nIn `src/index.js`, let's set `tracing: true` in the server configuration\n\n```js\n...\nconst server = new ApolloServer({\n  tracing: true,\n  typeDefs,\n  resolvers,\n  context,\n});\n```\n\nNow back in `Playground`, click the `TRACING` tab in the bottom right corner and run your query again. Here we can see exactly how long it took for each resolver to run.\n\n## N+1 Queries\n\nEach `artist` call might take about `170ms` depending on your Internet speed. For these 10 results, we're making 7 unnecessary calls. These will likely fire off concurrently since they're in the same execution level, but it's possible they could add an additional `1.2s` or more to the response time if we overload the API we're calling. Yikes!\n\nImagine how much worse this becomes when we change the `songs` search limit to `100`, `1000`, etc.\n\nWhat if we query for all songs on The Beatles' album `Abbey Road`? The `artist` resolver for `Song` will be calling the iTunes API **17** times for the exact same artist ID.\n\nThis could definitely cause performance issues for both our clients and our backend services, databases, etc. How can we fix this?\n\n## DataLoader (Batching \u0026 Caching)\n\n[DataLoader](https://github.com/facebook/dataloader#using-with-graphql) will coalesce all individual `load`s which occur within a single frame of execution (a single tick of the event loop) and then call your batch function with all requested keys. The result is cached on the request, so additional calls to `load` for the same key on the same request will return the cached value.\n\nLet's kill our server to install `dataloader` and start it back up\n\n```bash\n$ npm install dataloader\n$ npm start\n```\n\nIn `src/index.js`, we'll want to `require` dataloader at the top\n\n```js\nconst DataLoader = require('dataloader');\n```\n\nWe'll also want to change our `context` to include a `loaders` field so they can be used in all `resolvers`.\n\nOur `context` is just a static object, but we'll need a new context to be generated for each request so our cache isn't [held across requests](https://github.com/facebook/dataloader#creating-a-new-dataloader-per-request). This is generally a good idea whether you're using DataLoaders or not. You might want to have a cache in your connectors themselves, but those caches generally shouldn't be shared across requests or between different users. So let's make `context` a function!\n\n```js\nconst context = () =\u003e {\n  const connectors = createConnectors();\n  const loaders = {};\n\n  return { connectors, loaders };\n};\n\nconst server = new ApolloServer({\n  tracing: true,\n  typeDefs,\n  resolvers,\n  context,\n});\n```\n\nand we'll create our first loader for `artist`\n\n```js\nconst context = () =\u003e {\n  const connectors = createConnectors();\n  const loaders = {\n    artist: new DataLoader(IDs =\u003e Promise.resolve(\n      IDs.map(id =\u003e connectors.iTunes.artist({ id })),\n    )),\n  };\n\n  return { connectors, loaders };\n};\n```\n\nNow let's modify our `artist` field resolver under the `Song` root object to use the loader\n\n```js\nSong: {\n  ...\n  artist: ({ artistId }, _, ctx) =\u003e (\n    ctx.loaders.artist.load(artistId)\n  ),\n},\n```\n\nOpen the [Playground](http://localhost:4000) again and send the same query for `songs` with artist details\n\n```graphql\n{\n  songs(name: \"Sun\") {\n    id\n    name\n    album\n    url\n    artist {\n      id\n      name\n      url\n      genre\n    }\n  }\n}\n```\n\nEach artist ID should only be looked up once! 🎉\n\nWe can also solve this problem using a memoization cache instead of a DataLoader. [Apollo](https://www.apollographql.com/) built the [RESTDataSource](https://www.apollographql.com/docs/apollo-server/features/data-sources) to use a memoization cache on `GET` requests in order to solve this problem and I think it's more straightforward than using a DataLoader. We would just need to rewrite our connectors to `extend RESTDataSource`. I'll leave that exercise up to you!\n\nEven with a RESTDataSource, a DataLoader is still useful for [batching requests](https://www.apollographql.com/docs/apollo-server/features/data-sources/#batching) to APIs that support a batch endpoint (something like `getArtistsByIDs`).\n\n## 🚀 Conclusion\n\nYou've reached the end of this introductory GraphQL workshop.\n\nHow do you feel? Heck, I'm proud of you!\n\nToday you learned about:\n\n* GraphQL servers (Apollo)\n* GraphQL tools (Playground, tracing, graphql-import)\n* Organizing GraphQL projects\n* Queries\n* Schema / Types\n* Resolvers\n* Context\n* Connectors (and making HTTP requests)\n* Execution levels (and passing data down)\n* Field Arguments\n* Query Variables\n* Nullability\n* Throwing Errors\n* Deprecating Fields\n* Graph Relationships\n* Naive Pitfalls (N+1 Queries)\n* ... and how to solve those with DataLoaders or RESTDataSource\n\nYou can see the entire completed API project in the [complete branch](https://github.com/nathanchapman/graphql-music/tree/complete).\n\nIf you liked this workshop, please give it a ⭐️ and [follow me](https://github.com/nathanchapman) for more content like this!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnathanchapman%2Fgraphql-music","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnathanchapman%2Fgraphql-music","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnathanchapman%2Fgraphql-music/lists"}