GraphQL notes
This commit is contained in:
parent
1b6e91f384
commit
1b35ea8fc6
3 changed files with 340 additions and 6 deletions
|
@ -6,3 +6,240 @@ tags: [graph-ql]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Creating a GraphQL server
|
# Creating a GraphQL server
|
||||||
|
|
||||||
|
> We will use Node.js to create a basic GraphQL server that will serve data from a product database
|
||||||
|
|
||||||
|
Our server will allow us to add products to a database through a mutatation and to query the products that we have added. We will use a JS object instead of a real database.
|
||||||
|
|
||||||
|
## Create a basic Express server
|
||||||
|
|
||||||
|
First we create a basic server in Node using Express:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import express from "express";
|
||||||
|
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.send("Graph QL test project");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(8080, () =>
|
||||||
|
console.log("Running server on port localhost:8080/graphql")
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Add GraphQL as middlewear
|
||||||
|
|
||||||
|
Next we introduce GraphQL as a piece of Node.js [middlewear](/Programming_Languages/Node/Architecture/Middleware.md), with the `app.use()` method.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { graphqlHTTP } from "express-graphql";
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
"/graphql",
|
||||||
|
graphqlHTTP({
|
||||||
|
schema: schema,
|
||||||
|
rootValue: resolvers,
|
||||||
|
graphiql: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `schema` is a reference to our GraphQL schema - the structure of the fields that define our server. This is not yet defined.
|
||||||
|
- `rootValue` is a reference to our resolvers. This is not yet defined.
|
||||||
|
- `graphiql` is the GUI tool that will be served from the GraphQL endpoint at `localhost:8080/graphql`. This tool enables us to interrogate our data using the defined schema and see what data we would get back from frontend queries.
|
||||||
|
|
||||||
|
## Resolvers
|
||||||
|
|
||||||
|
We will specify our resolvers in a dedicate resolver file. In GraphQL you need to define resolvers for both your queries and your mutations.
|
||||||
|
|
||||||
|
To achieve this we will have a dummy object as the database containing our products and a class working as a generator function that will create a product object with certain properties, individuated by an id. We will invoke this class to create new products for the database and to retrieve existing products from the database.
|
||||||
|
|
||||||
|
First we create the product class and the database object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// resolvers.js
|
||||||
|
|
||||||
|
const productDb = {};
|
||||||
|
|
||||||
|
class Product {
|
||||||
|
constructor(id, { name, description, price, soldout, stores }) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
this.price = price;
|
||||||
|
this.soldout = soldout;
|
||||||
|
this.stores = stores;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next we define a resolver for read operations. This will receive a product id and return the corresponding product back from the database as an instance of `Product`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// resolvers.js
|
||||||
|
|
||||||
|
const resolvers = {
|
||||||
|
getProducts: ({ id }) => {
|
||||||
|
return new Product(id, productDb[id]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Next we declare a resolver that will handle our mutation: adding new products to the database.
|
||||||
|
|
||||||
|
> Mutations in GraphQL are the equivalent of `POST`, `PUT`, and `DELETE` in REST APIs. In other words, they are the means by which we update the data that the GraphQL Server exposes.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// resolvers.js
|
||||||
|
|
||||||
|
const resolvers = {
|
||||||
|
getProducts: ({ id }) => {
|
||||||
|
return new Product(id, productDb[id]);
|
||||||
|
},
|
||||||
|
createProduct: ({ input }) => {
|
||||||
|
let id = guid; // imagine a hash function here
|
||||||
|
productDatabase[id] = input;
|
||||||
|
return new Product(id, input);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
This handles the backend mechanics of reading from and writing to the database, but we need still need to integrate it with the GraphQL middlewear. We do this through the GraphQL server's schema file.
|
||||||
|
|
||||||
|
The GraphQL Schema, defined on the backend, describes the shape of queries that can be run against the GraphQL Server. A schema is a series of fields matched to a type specification. Writing a schema is just like defining a type or interface in TypeScript or a schema in Mongoose.
|
||||||
|
|
||||||
|
### Define `Product` type
|
||||||
|
|
||||||
|
First we will define a schema entry for products:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// schema.js
|
||||||
|
import { buildSchema } from "graphql";
|
||||||
|
|
||||||
|
const schema = buildSchema(`
|
||||||
|
type Product {
|
||||||
|
id: ID
|
||||||
|
name: String
|
||||||
|
description: String
|
||||||
|
price: Float
|
||||||
|
soldout: Boolean
|
||||||
|
stores: [Store]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Store {
|
||||||
|
store: String
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note that here we define a **custom** `Store` type that integrates with the the `Product` type as an array of stores. This is a required field, indicated by the `!`. Also the `ID` type is special ...
|
||||||
|
|
||||||
|
// TODO: Explain why the ID type is in caps. Is it the equivalent of the primary key?
|
||||||
|
|
||||||
|
### Define `Query` method
|
||||||
|
|
||||||
|
So far we have established the types necessary to service our `getProduct` resolver but we have not provided a declared means of querying. We do this by declaring a `Query` type that will invoke the `getProduct` resolver we defined earlier. Now the server knows that when a `Query` is run, it must use that resolver:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const schema = buildSchema(`
|
||||||
|
...
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
getProducts(id: ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Define `Mutation`
|
||||||
|
|
||||||
|
So far we have defined the fields necessary to query the GraphQL server using the `getProduct` resolver but we have not yet provided a way to mutate the data by adding new products. We need to integrate our `createProduct` resolver.
|
||||||
|
|
||||||
|
We do this by defining a `Mutation` type that references the `createProduct` resolver. And we type its paramter and return value:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const schema = buildSchema(`
|
||||||
|
...
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createProduct(input: ProductInput): Product
|
||||||
|
}
|
||||||
|
|
||||||
|
input ProductInput {
|
||||||
|
id: ID
|
||||||
|
name: String
|
||||||
|
description: String
|
||||||
|
price: Float
|
||||||
|
solout: Boolean
|
||||||
|
stores: [StoreInput]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoreInput {
|
||||||
|
store: String
|
||||||
|
}
|
||||||
|
|
||||||
|
`);
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note that the `input` parameter to the mutation and the `input` keyword are closely coupled. Note also that although `ProductInput` / `Product` and `Store` / `StoreInput` are identical in terms of their shape, we must still create dedicated new types. We cannot mix the types from the different resolvers.
|
||||||
|
|
||||||
|
## Using the server
|
||||||
|
|
||||||
|
Our server is now complete and will allow us to send and receive data of the following shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1234,
|
||||||
|
"name": "Product A",
|
||||||
|
"description": "This is Product A",
|
||||||
|
"price": 34.99,
|
||||||
|
"soldout": false,
|
||||||
|
"stores": [
|
||||||
|
{
|
||||||
|
"store": "London"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"store": "Sheffield"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"store": "Lincoln"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We will now switch to the client-side and see how we can go about adding and querying products.
|
||||||
|
|
||||||
|
### Adding new products through a mutation
|
||||||
|
|
||||||
|
We can invoke our mutation resolver by sending the following query to the server:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
mutation {
|
||||||
|
createProduct(
|
||||||
|
input: {
|
||||||
|
name: "Widget4"
|
||||||
|
description: "Lorem ipsum"
|
||||||
|
price: 39.99
|
||||||
|
soldout: false
|
||||||
|
stores: [{ store: "London" }]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is represented in GraphiQL as follows:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
// TODO: Add bit about the value we want returned see 8:20 and explain what is returned
|
||||||
|
|
||||||
|
### Returning a product through a query
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
// Add new image of this working in GraphiQL
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
title: Key characteristics of GraphQL
|
title: Key characteristics of GraphQL
|
||||||
categories:
|
categories:
|
||||||
- Databases
|
- Databases
|
||||||
tags: [graph-ql]
|
tags: [graphql, APIs]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Key characteristics of GraphQL
|
# Key characteristics of GraphQL
|
||||||
|
@ -18,10 +18,12 @@ tags: [graph-ql]
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
From the point of view of the frontend, GraphQL is a query language that may be used via wrappers for the given frontend framework or language (JavaScript, React, .NET etc). You can make direct `fetch` requests to a GraphQL server or use a third-party dedicated client like Apollo (specifically designed for React).
|
From the point of view of the frontend, GraphQL is a query language that may be used via wrappers for a given framework or programming language (such as Apollo) or directly simply by sending requests over HTTPS to a single URL.
|
||||||
|
|
||||||
From the point of view of the backend, GraphQL is a **runtime** that provides a structure for servers to describe the data to be exposed in their APIs. We call this structure the **schema**. For example, the GraphQL server might translate the query into SQL statements for a relational database then take what the storage engine responds with, translate it into JSON and send it back to the client application.
|
From the point of view of the backend, GraphQL is a **runtime** that provides a structure for servers to describe the data to be exposed in their APIs. We call this structure the **schema**. For example, the GraphQL server might translate the query into SQL statements for a relational database then take what the storage engine responds with, translate it into JSON and send it back to the client application.
|
||||||
|
|
||||||
|
> The backend implementation of GraphQL is known as the **GraphQL server**. This server is distinct from any of the physical servers that the backend may rely on. The client queries the GraphQL server.
|
||||||
|
|
||||||
Client requests are sent over HTTPS and the data is typically returned in the form of JSON:
|
Client requests are sent over HTTPS and the data is typically returned in the form of JSON:
|
||||||
|
|
||||||

|

|
||||||
|
@ -39,16 +41,62 @@ So, for comparison, a query would be akin to a `READ` instruction in SQL or a `G
|
||||||
|
|
||||||
There is a third request type called a **subscription**. This is used for real-time data monitoring requests, like a data stream, similar to a continuous `READ` process. Mutations typically trigger events that can be subscribed to.
|
There is a third request type called a **subscription**. This is used for real-time data monitoring requests, like a data stream, similar to a continuous `READ` process. Mutations typically trigger events that can be subscribed to.
|
||||||
|
|
||||||
### Structure and behaviour
|
### Frontend
|
||||||
|
|
||||||
We define the structure of a GraphQL API through the schema. A schema is strongly typed and is basically a graph of fields that have types, e.g
|
Below is an example of a request that would be made to a GraphQL server from the frontend:
|
||||||
|
|
||||||
```graphql
|
```graphql
|
||||||
{
|
query {
|
||||||
name: String
|
employee(id: 42) {
|
||||||
|
name
|
||||||
|
email
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Backend: structure and behaviour
|
||||||
|
|
||||||
|
We define the structure of a GraphQL API on the backend through the schema. A schema is strongly typed and is basically a graph of fields that have types, e.g
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type Employee(id: Int!) {
|
||||||
|
name: String!
|
||||||
|
email: String!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`!` stands for required. In addition to the standard primitive data-types you can also have custom types.
|
||||||
|
|
||||||
We implement the behaviour of the API through functions called **resolver functions**. Each field in a GraphQL schema is backed by a resolver function. A resolver function defines what data to fetch for its field.
|
We implement the behaviour of the API through functions called **resolver functions**. Each field in a GraphQL schema is backed by a resolver function. A resolver function defines what data to fetch for its field.
|
||||||
|
|
||||||
> A resolver function represents the instructions on how and where to access raw data. For example, a resolver function might issue a SQL statement to a relational database, read a file’s data directly from the operating system, or update some cached data in a document database. A resolver function is directly related to a field in a GraphQL request, and it can represent a single primitive value, an object, or a list of values or objects.
|
> A resolver function represents the instructions on how and where to access raw data. For example, a resolver function might issue a SQL statement to a relational database, read a file’s data directly from the operating system, or update some cached data in a document database. A resolver function is directly related to a field in a GraphQL request, and it can represent a single primitive value, an object, or a list of values or objects.
|
||||||
|
|
||||||
|
### GraphQL endpoint
|
||||||
|
|
||||||
|
The GraphQL server exposes a single endpoint as a URL. This is in contrast to a REST API where there are multiple endpoints, each corresponding to a different resource. With GraphQL, the request is not specified by the URL and the payload but rather in the GraphQL string that you send to the endpoint, like the `employee` query above.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
### Single requests
|
||||||
|
|
||||||
|
With a REST API if you require multiple resources you have to make multiple requests to different endpoints and then once they resolve, synthesise them on the client side. With GraphQL you can send a single request that encompasses each individual resource:
|
||||||
|
|
||||||
|
The REST scenario:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The GraphQL scenario:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Abstraction of multiple services
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Stops overfetching
|
||||||
|
|
||||||
|
In a strict REST API you must request all resources from a given resource and then filter on the client. With GraphQL you request only the resource you need at the client-level.
|
||||||
|
|
||||||
|
### Single endpoint
|
||||||
|
|
||||||
|
In GraphQL you have a single endpoint and it always remains the same. This makes updates easier to manage, since you don't need to broadcast new endpoints as the API grows. It also simplifies frontend parsing, since you don't need to account for, or interpolate different endpoints in your request URL.
|
||||||
|
|
49
Databases/GraphQL/Schema_Definition_Language.md
Normal file
49
Databases/GraphQL/Schema_Definition_Language.md
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
categories:
|
||||||
|
- Databases
|
||||||
|
tags: [graph-ql]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Schema Definition Language
|
||||||
|
|
||||||
|
SDL is the formal name for the syntax of GraphQL schemas.
|
||||||
|
|
||||||
|
A schema is a collection of object types that contain fields. Each field has a type of its own. A field's type can be a primitive/scalar value (such as an Int or a String), or it can be another object type (just like a custom type in TS).
|
||||||
|
|
||||||
|
A schema's type can be non-nullable which is to say, a required field. We indicate this with `!`.
|
||||||
|
|
||||||
|
A type for a field can be a collection/array of a given type.
|
||||||
|
|
||||||
|
The following example indicates these properties:
|
||||||
|
|
||||||
|
```gql
|
||||||
|
type Person {
|
||||||
|
age: Int
|
||||||
|
name: String
|
||||||
|
pets: [Pet]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pet {
|
||||||
|
species: String
|
||||||
|
name: String
|
||||||
|
age: Int
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Descriptions
|
||||||
|
|
||||||
|
Descriptions are comments that allow you to document your Schema
|
||||||
|
|
||||||
|
Single line:
|
||||||
|
|
||||||
|
```gql
|
||||||
|
"Single line comment"
|
||||||
|
```
|
||||||
|
|
||||||
|
```gql
|
||||||
|
"""
|
||||||
|
Multi
|
||||||
|
line
|
||||||
|
comment
|
||||||
|
"""
|
||||||
|
```
|
Loading…
Add table
Reference in a new issue