1. 24
    1. 10

      I feel the headline doesn’t really match with the conclusion of the post, which ends by saying that some of their teams love and it some don’t. This ends in a very reasonable place, which is that there is no one size fits all solution and you need to weigh your needs when deciding between different technologies.

      After running a large GraphQL deployment for the last few years, we’ve run into most of the issues outlined in the post. All things considered, I’d make the same choice again for our use case when imagining the “REST: From Excitement to Deception” version of this.

      1. 16

        I think the intended title may have been “From Excitement to Disappointment”, as “déception” in French is “disappointment”, it’s an easy mistake to make (the author has a French name).

    2. 4

      You shouldn’t make architectural choices to save milliseconds

      Sure if you hate your users and want slow sites. Any architecture can deliver an api or a website. The main thing that distinguishes a good api is comprehensibility to the consumer and latency (or rarely throughput).

      The rest of the article’s complaints amount to someone who refuses work with the grain of a new tool and wish it were the same as the old tool.

      In particular the refusal to use dataloaders and the complaint that using the orm results in n+1 queries are something special. Almost every technology is bad if you insist on using it in the exact opposite way that it’s intended to be used.

      1. 9

        The n+1 problem is real. It’s not a problem with GraphQL per se, but with the way that it’s implemented on the server with independent resolver functions. To avoid n+1, there needs to be cooperation between the parent and child field resolvers so the parent can issue a join query that returns the data of the child fields. This is tricky; I’ve implemented ad hoc solutions but what’s really needed is a kind of query optimizer that restructures GQL queries with knowledge of the underlying db schema. I’m not aware of such a thing.

        (The usual solution seems to be “dataloaders”, which afaict are just putting a big cache in the middle to reduce the coefficient of the ‘n’ in ‘n+1’. I guess it works but it seems like a kludge to me.)

        1. 3

          At least in my experience, dataloaders are different than just a cache. When a child resolver calls the dataloader, the actual DB query doesn’t run immediately: through some JS async magic, the dataloader waits until all the other child resolvers have called it. Then it runs a single DB query that gets everything at once, along the lines of WHERE id IN (...). You still have n+1 function calls, but the number of trips to the DB is constant: 1 for the parent and 1 for all the children.

          Of course, it’s up to the programmer to write the dataloaders, and there’s nothing stopping you from writing code that shoves O(n) queries through a cache. There’s also nothing forcing you to write the dataloader in the first place, which I think is the larger problem in practice: the “correct” way requires more work.

          1. 1

            Huh, ok. That’s similar to what I’ve done, having the parent resolver look at what sub-fields are present in the query (that info can be found in the context) and running a different (joined) db query if there are subfields that refer to joined tables.

            1. 1

              You may find that performs worse than running the queries independently, depending on whether or not your database is parallelizing execution. Once the data is fetched into the data loader for a column, your resolvers are effectively performing an in memory join.

      2. 3

        Yeah, it sounds like “we chose shitty tools so we blame the architecture”. Generating a query per model (or even just one query period) should be doable regardless of the API.

        Edit: On second thought, this certainly grants some credibility to the claim that the ecosystem isn’t mature enough, if this is an off-the-shelf tool. That’s just horrible and inexcusable.

        1. 2

          There are loads of data loader libraries. Rolling your own is also not hard.

          Graphql is pretty old at this point (10+ years iirc). The main gotcha I’ve found is that if you’re dealing with a data store that speaks json, you have to deal with the overhead of parsing that rather than just passing that through.

    3. 2

      I admit that I bought into the GraphQL hype when I first heard all the glowing reports about it. I thought it was conceptually very elegant. I was pretty tired of explaining Roy Fielding’s ideas to API authors who weren’t interested in learning words like “idempotent.” REST, to them, was just another way of doing RPCs. I really hoped that GraphQL’s momentum would eventually lead to the complete replacement of REST. Now that I’ve actually built and used a GraphQL API, I still think it’s great, but whether I would use it again would depend on the project. As the article’s author points out, it’s definitely more complicated than REST; built-in pagination is a glaring omission; and the browser dev tooling is still pretty poor. The proliferation of OpenAPI’s JSON schemas has also helped level the playing field in REST’s favor where strong typing is concerned.

      In its defense, I don’t think the n+1 problem is especially unique to GraphQL. REST API authors often set endpoints up to represent individual resources, where they interpret “resource” to mean an individual record in a table. (I think Roy Fielding had something more abstract in mind, but whatever.) The client has to make exponentially more requests for each layer of nested data and then stitch it all together themselves. It may not look like an n+1 problem from the API author’s perspective because it’s been moved to the client. A more mature REST API author might build ad hoc REST endpoints optimized for very specific UX requirements. And that’s probably OK for a lot of projects, but it’s brittle. Things get messy when you start adding more clients with differing UX requirements. To avoid code duplication, existing endpoints are often overloaded with more data. Some years later, someone might want to prune that data to improve performance, but no one is really sure which data is safe to prune, so no one does it.

      GraphQL is great for situations like this. It helps keep services that resolve nested queries organized and shifts the burden of query optimization to the server. This doesn’t make optimization easy, but it does make it more flexible. One relatively easy optimization is to set resolvers up in such a way as to not make n+1 queries to the database if the client query isn’t asking for that nested data in the first place. The whole point of GraphQL, after all, is to only resolve the data that the client asks for and no more. This gives the client the flexibility to experiment with the performance/UX tradeoff between lazy and eager loading.

    4. 2

      Exposing directly an internal database schema directly through your API is a bit dangerous as it creates a strong coupling with the storage. ORM, like Prisma or Graphile, has native integration with GraphQL pushing this idea further.

      We went with GraphQL via Hasura, and while Hasura isn’t representative of GraphQL as a whole, Hasura’s base offering took about a year to turn from best practice to deprecated. In that time, we grew from 10 engineers to 20.

      A “benefit” of GraphQL is that your frontend and backend engineers are more decoupled and can communicate less. However, this also means that backend engineers are not naturally motivated to understand frontend needs.

      Our DB schema quickly became unergonomic for frontend consumers, yet because the DB schema was directly coupled with the frontend, we wrote repetitive ad-hoc data transformations all over the frontend to massage the GraphQL schema to a higher level data model.

      1. 6

        So…don’t do that. The downside of any solution that turns your database into an api is that your database needs to be designed to present a good api. This is true whether or not you’re exposing a graphql or rest api.

        It’s somewhat less painful if you’re doing real rest (as opposed to slashes-are-what-makes-rest), because the tables or views can be the resources and the mapping may be fairly natural.

        However, this also means that backend engineers are not naturally motivated to understand frontend needs.

        This seems like a problem in the organization not the technology. Are you all trying to deliver the same product? If yes, why aren’t you measuring the impact of the backend on the the performance of the frontend?

        In my experience, wish-it-was-rest apis either force the frontend to make many calls or result in joining lots of redundant data onto endpoints, impacting speed.

        1. 2

          So…don’t do that.

          For me, this wasn’t about whether a better result was possible; of course one is possible. But the happy path of Hasura led to the results that we got, and in deciding a path forwards, one of our evaluations was to do GraphQL better, without DB->API and with intentional modeling of resources and mutations. We decided to do something else instead.

          This seems like a problem in the organization not the technology.

          It is both a technological and organization problem; the challenges that came along with Hasura specifically weren’t a good match with our organization. We preferred a technological solution that intrinsically motivated better backend<>frontend working patterns instead of a solution that required additional work to motivate the desired outcomes.

          1. 2

            We preferred a technological solution that intrinsically motivated better backend<>frontend working patterns

            Partly, I feel that this does seem to reflect an organizational failure, and that introducing friction just to make developers do their damn job is wasteful and defeatist. On the other hand, there is something interesting about this scenario. I wonder if there is a name for it? I am often a proponent for introducing constraints that some might experience as friction in their day to day work, because I think it leads to better outcomes and less friction over time.

      2. 6

        The first thing to do when exposing an API that is automatically mapped to a database (which I am a big believer in) is to set up a separate view-only schema (ie a schema containing only database views), and expose only the data that is relevant for the client, possibly with some denormalization and aggregation as needs arise.

        we wrote repetitive ad-hoc data transformations all over the frontend

        The place to do that is in the exposed specialized schema. The underlying schema with the actual tables remains decoupled from the client.

      3. 2

        A “benefit” of GraphQL is that your frontend and backend engineers are more decoupled and can communicate less.

        Only if you treat GraphQL as a way to mechanically expose a low-level interface (like your DB). So don’t do that!

        We switched to GraphQL as part of a push to get frontend and backend working more closely. We’ve found the schema to be a very helpful locus of collaboration; when engineers from either side can propose interfaces using the shared language of the schema (which requires concreteness specificity), it’s much less common to end up in situations where the two teams are talking past each other.

        we wrote repetitive ad-hoc data transformations all over the frontend to massage the GraphQL schema to a higher level data model.

        If a schema requires its consumers to do this kind of transformation, I would argue that it’s not well-designed (for those consumers). Sounds like your GraphQL schema should have exposed a higher-level API to begin with. (A design goal for our GraphQL API is that the frontend should not need to do any significant reshaping of data it receives from the backend, and in particularly it should never need to do its own joining, filtering, or sorting.)

        There are plenty of issues with GraphQL (weird and oddly-specified serialization, clunky and limiting type system) but so much of the criticism I see boils down to “sometimes people make bad APIs using GraphQL,” which, sure. Designing a good API is still a problem that requires human thought and intention; no technology is going to do that for you.

        1. 1

          I believe a lot of the problems originated with our out-of-the-box use of Hasura, directly exposing the underlying schema. It was definitely not well designed. In our case the problem was that the bad API was the happy path, and that I believe is Hasura-specific, not GraphQL-specific.