Wechat search [front-end full stack developer] pay attention to this hair loss, stall, selling goods, continuous learning of the programmer, the first time to read the latest article, will give priority to two days to publish new articles. Attention can be a gift package, you can save a lot of money!

Blog.heroku.com by CHRIS CASTLE

Over the past few years, GraphQL has become a very popular API specification that focuses on making data acquisition easier for clients, whether front-end or third-party.

In the traditional REST-based API approach, the client makes a request and the server decides to respond:

curl https://api.heroku.space/users/1

{
  "id": 1."name": "Luke"."email": "[email protected]"."addresses": [{"street": "1234 Rodeo Drive"."city": "Los Angeles"."country": "USA"}}]Copy the code

However, in GraphQL, the client can determine exactly what data it is fetching from the server. For example, the client might only need the username and email, without any address information:

curl -X POST https://api.heroku.space/graphql -d '
query {
  user(id: 1) {
    name
    email
  }
}


{
  "data":
    {
    "name": "Luke"."email": "[email protected]"}}Copy the code

With this new model, customers can scale back responses to meet their needs and make more efficient queries to the server. For single-page applications (SPA) or other front-end heavy client applications, rendering times can be accelerated by reducing the payload size. However, like any framework or language, GraphQL has trade-offs. In this article, we’ll explore the pros and cons of using GraphQL as the query language for the API, and how to start building the implementation.

Why GraphQL?

As with any technical decision, it’s important to understand what GraphQL offers your project, and not simply choose it because it’s a buzzword.

Consider a SaaS application that uses aN API to connect to a remote database. If you want to render a user’s profile page, you might need to make an API GET call to GET information about the user, such as a user name or email. Then, you might need to make another API call to get information about the address, which is stored in a different table. As your application evolves, you may need to continue making more API calls to different locations because of the way it’s built. While each API call can be done asynchronously, you also have to deal with their responses, whether it’s errors, network timeouts, or even pausing page rendering until all the data is received. As mentioned above, the payload of these responses may exceed the need to render your current page, and each API call has network latency, which can add up to a considerable amount.

With GraphQL, instead of making multiple API calls (like GET /user/:id and GET /user/:id/addresses), you make one API call and submit the query to a single endpoint:

query {
  user(id: 1) {
    name
    email
    addresses {
    street
    city
    country
    }
  }
}
Copy the code

Then, GraphQL provides just one endpoint to query all the domain logic needed. If your application grows, you’ll find yourself adding more data stores to your architecture — PostgreSQL might be a good place to store user information, Redis might be a good place to store other kinds of information — and a call to the GraphQL endpoint will address all these different locations, And respond to the client with the data they request.

GraphQL is also useful here if you’re not sure what your application needs and how to store data in the future. To modify the query, you simply add the name of the desired field:

		addresses {
      street
+     apartmentNumber   # new information
      city
      country
    }
Copy the code

This greatly simplifies the process of developing your application over time.

Define a GraphQL Schema

There are GraphQL server implementations in various programming languages, but before you can start, you need to identify objects in your business domain, just like any API. Just as REST apis might use JSON schemas, GraphQL defines its schemas using SDL or Schema definition languages, which is an idempotent way of describing all the objects and fields available to the GraphQL API. The general format of an SDL entry is as follows:

type $OBJECT_TYPE {
  $FIELD_NAME($ARGUMENTS): $FIELD_TYPE
}
Copy the code

Building on the previous example, let’s define what the user and address entries look like.

type User {
  name:     String
  email:    String
  addresses:   [Address]
}

type Address {
  street:   String
  city:     String
  country:  String
}
Copy the code

User defines two String fields, name and email, and includes a field called addresses, which is an array of addresses objects. Addresses also defines several fields of its own. (By the way, the GraphQL schema has not only object, field, and scalar types, but much more. You can also combine interfaces, unions, and parameters to build a more complex model, which I won’t cover in this article.)

We also need to define a type, which is the entry point to our GraphQL API. As you remember, a GraphQL query looks like this:

query {
  user(id: 1) {
    name
    email
  }
}
Copy the code

The Query field belongs to a special reserved type called Query, which specifies the main entry point to get the object. (There are also Mutation types for modifying objects.) Here, we define a user field that returns a User object, so our schema also needs to define this field:

type Query { user(id: Int!) : User } type User { ... } type Address { ... }Copy the code

The arguments in the field are comma-separated lists of the format $NAME: $TYPE. ! The way GraphQL indicates that this parameter is required, and the omission indicates that it is optional.

The process of incorporating this pattern into the server will vary depending on the language you choose, but in general, using the information as a string is sufficient. Node.js has the GraphQL package to prepare the GraphQL schema, but we’ll use the GraphQL-tools package instead because it offers some more benefits. Let’s import the package and read our type definitions to prepare for future development:

const fs = require('fs')
const { makeExecutableSchema } = require("graphql-tools");

let typeDefs = fs.readFileSync("schema.graphql", {
  encoding: "utf8".flag: "r"});Copy the code

Setting up the parser

Schema sets the way queries are built, but creating a schema to define the data model is only part of the GraphQL specification. The other part involves actually getting the data, which is done using a parser, a function that returns the underlying value of a field.

Let’s look at how to implement a parser in Node.js. Our goal is to consolidate the concept around how the parser works with the schema, so we won’t go into too much detail around how to set up the data store. In the “real world,” we might use something like KNEx for database connections. Now, let’s set up some virtual data:

const users = {
  1: {
    name: "Luke".email: "[email protected]".addresses: [{street: "1234 Rodeo Drive".city: "Los Angeles".country: "USA",}]},2: {
    name: "Jane".email: "[email protected]".addresses: [{street: "1234 Lincoln Place".city: "Brooklyn".country: "USA",},],},};Copy the code

The GraphQL parser in Node.js is equivalent to an Object, where key is the name of the field to retrieve and value is the function that returns the data. Let’s start with a simple example of initial user lookups by ID:

const resolvers = {
  Query: {
    user: function (parent, { id }) {
      // User lookup logic}},}Copy the code

The parser takes two parameters: an object representing the parent (which is usually unused in the original root query) and a JSON object containing the parameters of the field passed to you. Not every field has parameters, but in this case, we will have parameters because we need to retrieve its users by user ID. The rest of the function is simple:

const resolvers = {
  Query: {
    user: function (_, { id }) {
      returnusers[id]; }}},Copy the code

You’ll notice that we didn’t explicitly define a parser for users or Addresses, and the GraphQL-Tools package was smart enough to map those for us automatically. We can override these if we choose, but now that we have defined our type definition and parser, we can set up our full schema:

const schema = makeExecutableSchema({ typeDefs, resolvers });
Copy the code

Running server

Finally, let’s run the demo! Since we are using Express, we can use the Express-GraphQL package to expose our pattern as the endpoint. The package takes two parameters: schema and root value, and it has an optional parameter graphiQL, which we’ll discuss later.

Use GraphQL middleware to set up the Express server on your preferred port as follows:

const express = require("express");
const express_graphql = require("express-graphql");

const app = express();
app.use(
  "/graphql",
  express_graphql({
    schema: schema,
    graphiql: true,})); app.listen(5000.() = > console.log("Express is now live at localhost:5000"));
Copy the code

The browser to navigate to http://localhost:5000/graphql, you should see an IDE interface. In the left pane, you can enter any valid GraphQL query you want, and on the right you’ll get the results.

That’s what GraphiQL: True provides: a convenient way to test your queries that you may not want to expose in production, but it makes testing much easier.

Try entering the query shown above:

query {
  user(id: 1) {
    name
    email
  }
}
Copy the code

To explore GraphQL’s typing capabilities, try passing a string instead of an integer for the ID parameter.

# this doesn't work query {user(id: "1") {
    name
    email
  }
}
Copy the code

You can even try requesting fields that don’t exist:

# this doesn't work query {user(id: 1) {
    name
    zodiac
  }
}
Copy the code

With just a few clear lines of code expressed in a Schema, a strongly typed contract can be established between client and server. This prevents your service from receiving bogus data and clearly indicates the error to the requester.

Performance considerations

While GraphQL solves a lot of problems for you, it doesn’t solve all the inherent problems of building apis. Caching and authorization, in particular, just need some premeditation to prevent performance issues. The GraphQL specification doesn’t provide any guidance for implementing either method, which means the onus is on you to build them.

The cache

Rest-based apis don’t need too much attention when caching because they can build on existing HTTP header policies used by other parts of the Web. GraphQL does not have these caching mechanisms, which imposes an unnecessary processing burden on repeated requests. Consider the following two queries:

query {
  user(id: 1) {
    name
  }
}

query {
  user(id: 1) {
    email
  }
}
Copy the code

In the absence of some kind of cache, just retrieving two different columns would result in two database queries to get User with ID 1. In fact, since GraphQL also allows aliases, the following query is valid and two lookups are performed:

query {
  one: user(id: 1) {
    name
  }
  two: user(id: 2) {
    name
  }
}
Copy the code

The second example shows how queries can be batch processed. To be fast and efficient, we want GraphQL to access the same database rows with as few roundtrips as possible.

The Dataloader package is designed to address both of these issues. Given an array of ids, we get all of them from the database at once; Again, subsequent calls to the same ID will fetch the item from the cache. To build this using dataloader, we need two things. First, we need a function to load all the requested objects. In our example, it looks like this:

const DataLoader = require('dataloader');
const batchGetUserById = async (ids) => {
   // In real life, this would be a database call
  return ids.map(id= > users[id]);
};
// userLoader is now our "batch loading function"
const userLoader = new DataLoader(batchGetUserById);
Copy the code

This solves the problem of batch processing. To load the data and use the cache, we’ll replace the previous data lookup with a call to the load method and pass in our user ID:

const resolvers = {
  Query: {
    user: function (_, { id }) {
      returnuserLoader.load(id); }},}Copy the code

authorization

Authorization is an entirely different issue for GraphQL. In short, it is the process of identifying whether a given user has access to certain data. We can imagine a scenario where authenticated users can perform queries to obtain their own address information, but should not be able to obtain the addresses of other users.

To solve this problem, we need to modify the parser function. In addition to the field’s parameters, the parser has access to its parent node, as well as special context values passed in that provide information about the currently authenticated user. Since we know that the address is a sensitive field, we need to modify our code so that the call to the user doesn’t just return a list of addresses, but actually calls some business logic to validate the request:

const getAddresses = function(currUser, user) {
  if (currUser.id == user.id) {
    return user.addresses
  }

  return [];
}

const resolvers = {
  Query: {
    user: function (_, { id }) {
      returnusers[id]; }},User: {
    addresses: function (parentObj, {}, context) {
      returngetAddresses(context.currUser, parentObj); ,}}};Copy the code

Again, we don’t need to explicitly define a parser for each User field, just the parser we want to modify.

By default, Express-GraphQL passes the current HTTP request as the value of the context, but it can be changed when setting up the server:

app.use(
  "/graphql",
  express_graphql({
    schema: schema,
    graphiql: true.context: {
      currUser: user // Currently authenticated user}}));Copy the code

Schema best practices

One area missing from the GraphQL specification is guidance on versioning patterns. As applications grow and change, their apis will change as well, most likely requiring deletion or modification of GraphQL fields and objects. But this disadvantage is also positive: by carefully designing your GraphQL Schema, you can avoid pitfalls such as naming inconsistencies and messy relationships that are obvious in REST endpoints that are easier to implement (and break).

In addition, you should try to separate the business logic from the parser logic. Your business logic should be the single source of facts for the entire application. It is tempting to perform validation checks in parsers, but as patterns grow, this becomes a difficult strategy to sustain.

When is GraphQL not appropriate?

GraphQL does not meet HTTP communication requirements as precisely as REST does. For example, GraphQL only specifies a status code — 200 OK — regardless of whether the query succeeds or not. A special error key is returned in this response for the client to parse and identify what went wrong, so error handling can be a bit tricky.

Again, GraphQL is just a specification, and it won’t automatically solve every problem your application faces. Performance issues won’t go away, database queries won’t get faster, and in general, you’ll need to rethink everything about your API: authorization, logging, monitoring, caching. Versioning your GraphQL API can also be a challenge, as the official specification currently does not support handling interrupt changes, which are an inevitable part of building any software. If you’re interested in exploring GraphQL, you’ll need to invest some time in learning how to best integrate it with your needs.

To learn more

The community rallied around this new paradigm and provided a great list of GraphQL resources for both front-end and back-end engineers. It can be used by both front-end and back-end engineers. You can also see what queries and types look like by making a real request at the official playgrounds.

We also have a Code[ISH] podcast set dedicated to GraphQL’s benefits and costs.