By Chris Castle


The original:
https://blog.heroku.com


Translator: Dunib

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

In the traditional REST-based API approach, the client makes the request and the server determines the response:

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

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

However, in GraphQl, the client can determine exactly what data it gets from the server. For example, a client might only need a username and E-mail, but not any address information:

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


{
  "data":
    {
    "name": "Luke",
    "email": "[email protected]"
    }
}

With this new pattern, customers can make more efficient queries to the server by scaling back their responses to meet their needs. For single-page applications (SPAs) 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 a query language for the API and how to start building the implementation.

Why did you choose GraphQl?

As with any technology decision, it’s important to understand what benefits GraphQl offers your project, rather than simply choosing 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 the user’s profile page, you may need to make an API GET call to GET information about the user, such as the user name or E-mail. Then, you might need to make another API call to get the information about the address, which is stored in a different table. As the application grows, you may need to continue to make more API calls to different locations because of the way it is 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 can be more than necessary to render your current page, and each API call has network latency, which can add up to a significant total latency.

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

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

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

GraphQl is also useful here if you’re unsure of your application’s requirements 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
    }

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

Define a GraphQl Schema

There are server implementations of GraphQl in various programming languages, but before you start, you need to identify the objects in your business domain, just like any API. Just as a REST API might use a JSON Schema, GraphQL defines its Schema using SDL or the Schema definition language, 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
}

Let’s define what the User and Address entries look like based on the previous example.

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

type Address {
  street:   String
  city:     String
  country:  String
}

User defines two String fields, name and email, and it also includes a field called addresses, which is an array of addresses objects. Addresses also defines several fields of its own. (Incidentally, the GraphQl schema has not only object, field, and scalar types, but more. You can also combine interfaces, unions, and parameters to build more complex models, but that won’t be covered in this article.)

We also need to define a type, which is the entry point for our GraphQl API. You’ll remember that the GraphQl query looks like this:

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

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

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

The arguments in the field are comma-separated lists in the format $NAME: $TYPE. ! Is the required way for GraphQL to indicate that the parameter is required, and omits to indicate that it is optional.

Depending on the language of your choice, the process of incorporating this pattern into the server will vary, but in general, using the information as a string will suffice. Node.js has the graphql package to prepare the graphql schema, but we’ll use the graphql-tools package instead, because it provides some additional benefits. Let’s import the package and read our type definition in preparation for future development:

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

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

Set up the parser

The schema sets up how queries are built, but establishing the schema to define the data model is only part of the GraphQL specification. The other part involves actually getting the data, which is done by using a parser, which is a function that returns the base value of the field.

Let’s take a look at how to implement a parser in Node.js. Our goal is to solidify concepts around how parsers work with schemas, so we won’t go into too much detail around setting up the data store. In the “real world,” we might use something like KNEX to set up a database connection. 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",
    },
    ],
  },
};

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

Const resolvers = {Query: {user: function (parent, {id}) {// User lookup logic},},}

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

const resolvers = { Query: { user: function (_, { id }) { return users[id]; }}},

You’ll notice that we don’t have an explicit resolver for users or Addresses. The graphql-tools package is smart enough to map these for us automatically. We can override these if we choose, but now that we have defined our type definitions and parsers, we can build our complete schema:

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

Run server

Finally, let’s run the demo! Because we are using Express, we can use the Express-GraphQl package to expose our schema as the endpoint. The package requires two parameters: schema and root value, and it has an optional parameter graphiql, which we will discuss later.

Set up the Express server on your preferred port using GraphQl middleware, as shown below:

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"));

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 query that you might not want to expose in a production environment, but it makes testing much easier.

Try entering the query shown above:

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

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

Query {user(id: "1") {name email}}

You can even try to request a field that doesn’t exist:

Query {user(id: 1) {name zodiac}}

With just a few lines of clean code expressed in a Schema, you can establish a strongly typed contract between the client and server. This prevents your service from receiving fake 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 problems inherent in building APIs. Caching and authorization, in particular, just need some contingency to prevent performance issues. The GraphQl specification does not provide any guidance for implementing either of these methods, which means that the onus is on you to build them.

The cache

REST-based APIs don’t need to be overly concerned with caching because they can build on the existing HTTP header policies used by the REST of the Web. GraphQl does not have these caching mechanisms, which would put an unnecessary processing burden on repeated requests. Consider the following two queries:

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

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

Just retrieving two different columns without some sort of cache results in two database queries to get User with ID 1. In fact, because GraphQl also allows aliases, the following query is valid and it also performs two lookups:

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

The second example exposes the problem of how to batch queries. To be fast and efficient, we want GraphQl to access the same database rows with as few round-trips as possible.

The DataLoader package is designed to address both of these issues. Given an array of IDs, we’ll get all of these IDs from the database at once; Again, subsequent calls to the same ID will fetch the item from the cache. To use DataLoader to build this, 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 the database call return ids.map(id => users[id]); }; // UserLoader is now our "batch load function" const UserLoader = new DataLoader(BatchGetUserById);

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

const resolvers = { Query: { user: function (_, { id }) { return userLoader.load(id); }},}

authorization

Authorization is a completely different issue for GraphQl. In short, it is the process of identifying whether a given user has access to certain data. Imagine a scenario in which an authenticated user can perform a query to get his or her own address information, but should not be able to get 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 can also access its parent node, as well as the special context values passed in that provide information about the current authenticated user. Since we know that 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 }) { return users[id]; }, }, User: { addresses: function (parentObj, {}, context) { return getAddresses(context.currUser, parentObj); ,}}};

Again, instead of explicitly defining a resolver for each User field, we can just define a resolver that we want to modify.

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

App. use("/graphql", express_graphql({schema: schema, graphiql: true, context: {currUser: user // currently authenticated user}}));

Schema best practices

One missing aspect of the GraphQl specification is the lack of guidance on versioning patterns. As applications grow and change, their APIs will also change, and it’s likely that GraphQl fields and objects will need to be removed or modified. But this downside is also a positive one: by carefully designing your GraphQl Schema, you can avoid pitfalls that are obvious in easier to implement (and break) REST endpoints, such as naming inconsistencies and messy relationships.

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

When does Graphql not work?

GraphQl does not meet the needs of HTTP communication as precisely as REST. For example, GraphQl specifies only one status code — 200 OK — regardless of whether the query succeeds or fails. 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; it doesn’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 interrupts that 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 gathered around this new paradigm and provided a great list of GraphQl resources for front-end and back-end engineers. Both front-end and back-end engineers can use it. You can also see what queries and types look like by making a real request at the official playground.

We also have a [Code (ish) podcast episodes] (https://www.heroku.com/podcas… , focusing on the benefits and costs of GraphQl.


WeChat search [front-end full stack developer] pay attention to this hair loss, stall, sell goods, continue to learn programmers, the first time to read the latest articles, will be priority two days published new articles. Attention can be a big gift package, can save you a lot of money!