GraphQL Fragments Are the Best Match for UI Components
blog.manifold.co
blog.manifold.co
Consider this query from the article:
query ProfilePageData {
user(handle: "manifoldco") {
...HeaderData
...SidebarData
...TweetListData
}
}
That's great if the ProfilePage component knows that it only contains the Header, Sidebar, and TweetList components. Since they're designed for static queries, both Relay and Apollo suffer from this shortcoming: the parent query needs to know about every component that could possibly add fragments ahead of time, and pull them all in. In my opinion, this really fails at fulfilling the promise of components – the parent shouldn't need to know what all its descendants do.What if the rendered components are more dynamic? Putting @skip directives on every field is not really an option (and then you need a matching query $variable for every directive.)
If you think of the query as more of a template, then you could have portions of the query that are like "template blocks" that other components could extend, e.g.:
query ProfilePageData {
user(handle: "manifoldco") {
${userFragments}
}
}
...then descendant components could have access to `userFragments` as an extension point. I'm not yet sure if it's a terrible idea, but I started a project just the other day to experiment with it: https://github.com/exogen/apollo-dynamic-queriesThat way the parent can just render whatever components it wants without having to worry about what data they require.
Consider the Artist.Name and Artist.Disambiguation components in my GitHub example. Notice how they don't need the `mbid` prop that the ancestor component provides. I want a component like that for every single field in my Artist schema. If every one of those components duplicated the artist query, they'd all need to be passed the `mbid`, because that's how it determines what artist to retrieve. It's possible to do some tricks with `context` like I'm doing now, but I don't really trust that it's a better solution than having one query execution point with an extendable query.
This is the API I want:
<Artist mbid="abc-123">
<Artist.Name />
<Artist.Disambiguation />
<SomeOtherComponentThatHasArtistFieldDescendants />
</Artist>
This is the API you're saying I would have to use with vanilla Relay and Apollo: <Artist mbid="abc-123">
<Artist.Name mbid="abc-123" />
<Artist.Disambiguation mbid="abc-123" />
<SomeOtherComponentThatHasArtistFieldDescendants mbid="abc-123" />
</Artist>Considering neither are possible with vanilla Relay and Apollo and I need to build these helper HOCs either way, I don't really see why what you're proposing is better. I had already considered them both and deliberately did not do it that way.
It’s easy with Relay. With Classic you render a RelayRenderer. In Relay Modern you render a QueryRenderer. Either way, you can issue more queries than just your root query and it’s as easy as rendering a React component.
Consider Ruby's method_missing and corresponding respond_to? methods [1]. method_missing may respond to a method dynamically, but respond_to? should return true if a method will be responded to. If you constrain yourself to normal methods, or carefully defined method_missing bodies, you can predict with 100% accuracy, but what if you do something like this?
class Whoops
def method_missing(method)
if method == :fancy and Random.rand > 0.5
"it's your lucky day!"
else
super(method)
end
end
def respond_to?(method, include_private = false)
if method == :fancy
true # Well, actually, "maybe?".
else
super
end
end
end
Temporarily ignoring the problem created by randomness: Implementing respond_to? methods is annoyingly redundant. In order to fully-automate the process of implementing respond_to?, you'd need to analyze a method_missing implementation. In the common case, this is static type inference. In the limit case, this is arbitrary abstract interpretation.However, effectful code, such as this random example, is provably impossible to analyze correctly! You need to have some conservative approximation. Returning true from respond_to? for the :fancy method means that respond_to? should actually be called might_respond_to?.
OK, now back to React and GraphQL: Luckily, over-fetching data is usually not that big a deal (as long as you're within bandwidth constraints). It's often desirable to! Since you might need that data soon when the view changes. Knowning this, it's safe to assume a method for assembling a GraphQL query from fragments can be conservative. That's essentially what a totally static, manual composition of query fragments does. If that's a tight enough approximation for you (like some other commenters have implied) then great! But what if it's not?
Luckily, virtual DOMs are dramatically simpler and less powerful than arbitrary method_missing implementations. If your render method is already pure, you can call it an extra time to use it as its own abstract interpretation. It will produce a virtual DOM, which you can then analyze yourself. For example:
let capture = (f) => {
let oldCreateElement = React.createElement;
try {
React.createElement = (component, props, ...children) => {
return {component, ...props, children};
};
return f();
} finally {
React.createElement = oldCreateElement;
}
};
class DynamicQuery extends React.Component {
graphql() {
let dom = capture(() => this.render());
return gatherQuery(dom);
}
render() {
...
}
}
Here, the gatherQuery method will walk the objects returned by our monkey-patched createElement method, composing query fragments along the way. By following the component references, you can find static methods (easy!), or even instantiate components to do recursive renders yourself (this gets hairy, since React doesn't really make any of this public API). You can do something pretty simple with this: Just hard code a few component types to make some limited dynamic decisions, fallback to static over-query beyond that. Or you can get really fancy and do recursive rendering and analysis, if you really want to.[1] http://blog.enriquez.me/2010/2/21/dont-forget-about-respond-...
But it's way more work than just passing fragments up the tree via `context` in `componentWillMount` or `componentDidMount`, which is relatively simple by comparison. :)
I can devote a few days to read the JSON-API spec (http://jsonapi.org/) and get a pretty good understanding of it. I wish there were a way to consume JSON-API based APIs in the same declarative way GraphQL provides.
I'm thinking that a library that "translates" GraphQL queries to JSON-API requests will be a great solution.
It's actually really easy, I encourage you to look into it! I've written plenty of APIs over the years and it's one of the more pleasant experiences I've had.
I wrote this simple 35-line GraphQL server implementing a demo schema in 2 minutes just while I was replying to your comment: https://gist.github.com/exogen/d5ddf86dd7f9efd15d3fabdace759...
The GraphQL related tools are top notch and implementing GraphQL server isn't any harder than a REST based server would be (one could even argue that it would be simpler). Even if you don't want to implement everything from scratch, tools like Postgraph[0] exists that pretty much automate it for you.
I made a simple tutorial[1] on wrapping an existing API with GraphQL here which goes through basics of settings up too.
[0] https://github.com/postgraphql/postgraphql [1] https://github.com/motleyagency/devday-tutorials/blob/master...
To be honest, I sort of wonder if the right place to implement the GQL interpretation and scheduling layer is in a service-worker.
That way, the client devs who love this complex query language can also support the queries they need and ensure their calling conventions don't violate performance boundaries in the gql server implementation.
That said, thinking of this in terms of client vs server devs isn’t useful - it’s not like anyone on the team want to see the servers burn. The beauty of graphql is it’s descriptiveness - you could create a query performance score and fail CI if your main page queries are hitting too high of a cost. A meet up in SF last year had some FB engineers discussing this - they do something along these lines before shipping their android/iOS apps.
I am aware of this, and it seems ridiculous to me. It discards the value of having a graph query language. I'm open to being wrong about this assessment.
Why am I using a new query language if it's going to end up being exactly the same as the old rest query environment?
> That said, thinking of this in terms of client vs server devs isn’t useful
Considering it from a typical labor division standpoint or by technical dependency and requirements isn't useful? I disagree.
> The beauty of graphql is it’s descriptiveness
Which you have said the "performant" way to handle is to discard and turn into a formulaic series of bytes that is indistinguishable form a new term encoding on restful POST bodies.
> you could create a query performance score and fail CI if your main page queries are hitting too high of a cost.
This is exactly how everyone already tools RESTful endpoints. Whole industries are built around scoring endpoints this way.
Inevitably my concern about GraphQL being hard to implement server side is inevitably met with Facebook's outer and inner solution. The outer solution: Just pretend it's graphql, really it's postbodies and we won't let you write custom queries except for column filtering (which these solutions WILL fetch).
The inner solution, it has been intimated to me, is they have custom GQL libraries written in Haskell using Haxl that they don't share because the industry is too busy being afraid of it. Sadly, these don't appear to be open source (and even if they were, a lot of shops would not be equipped to use them).
the client controls the APIs without you touching them. It is required that the client syncs up with the server but you don't have to touch backend code to change the set of fields you want (this works for internal clients, not open APIs, clearly).
If you're thinking "but I can just allow the client to select fields in my JSONAPI compliant endpoint", yes, you can, and when you add includes for nested fields, and nested selections, you basically get 90% of graphql :)
True, but until someone provides a great software kit for doing that work, I'm not going to allow arbitrary extensions.
Are these also "not that hard?"
I have no idea how I would go about implementing complexity caps though, but I guess I would do something like what GitHub has done for their own GraphQL API [2], which they explain better than I can.
[1]: https://github.com/apollographql/persistgraphql [2]: https://developer.github.com/v4/guides/resource-limitations/
Also, instead of multiplying node counts like GitHub does (which is pretty clever!), another simple option would be to look at the depth of the query (how many levels down is the deepest leaf), and fail if it's over some maximum. This is also very easy to do as you get the query AST in the `info` field of the resolver. (This one is less effective than the one above since depth doesn't totally match up with resource usage, fields can be aliased, etc. but you get the idea.)
Okay but... I guess my question is: why are you denying a client the right to make a complex query? Is it because all your queries are kinda slow and so you must hand-optimize them, leading to a combinatoric explosion of codepaths?
Or is it because your clients cannot judge how complex the queries they're making are? If so, isn't this actually a gap in the GQL spec? Lots of other query language implementations offer query description and estimation commands in their code.
Your proposed solution seems to me like it's brutal for your consumers. There's minimal indication of how quickly your complexity metric will rise in the query. You'd need to add ad-hoc per query&mutation arguments to push that query complexity cap up for legitimate uses.
No, it's because:
(1) This is a feature of literally every API, most of them just use the extremely blunt instrument of rate limiting (even if requesting the same simple scalar value field over and over again does not add any strain on the server, you'll be rate limited just the same). Why aren't you asking this same question about REST queries?
and
(2) The 'Graph' part of 'GraphQL' means that queries can theoretically request connected nested objects of nearly infinite depth. This doesn't require that anything about the query code be slow or needs to be hand-optimized, or that there be any complex codepaths, just that MORE JOINS == MORE WORK and MORE PAYLOAD, no matter how perfectly optimized it is. Why aren't you asking "why does REST deny clients the right to make as deeply nested queries as they need?"
Combinatoric explosions of complexity via a single query path are not a feature of every API.
> The 'Graph' part of 'GraphQL' means that queries can theoretically request connected nested objects of nearly infinite depth.
Thanks for this.
> just that MORE JOINS == MORE WORK and MORE PAYLOAD
So like SQL but without all the excellent query complexity tools or clarity around what precipitates a join?
> Why aren't you asking "why does REST deny clients the right to make as deeply nested queries as they need?"
Because RESTful APIs tend not to allow ad hoc graph traversal. When they do, it's because they're tunneling a graph query language. When they do (e.g., ElasticSearch) I (and we, as in the community at large)_DO ask these questions.
> Because RESTful APIs tend not to allow ad hoc graph traversal.
I think you're taking this graph part too literally. Almost every API has a "graph" of connected objects. GraphQL just makes it so that you can traverse them with a single query. REST endpoints tend to force you to make multiple queries to go back and fetch information about the entities whose IDs or URLs you received in earlier requests – thus the rate limiting. In both cases, combinoratic explosions (and infinite depth) are possible – REST just forces you to explode into more round-trips (and the server is likely doing even more duplicated work than it needs to to fulfill those subsequent requests).
If you wanted to simulate the ease-off aspect of REST requiring clients to return for multiple rate-limited round trips to get the query data they want, you could simply add a timeout in the nested object's GraphQL resolvers that perform self-rate-limiting. Same result but the clients don't need to know about it, they can just wait the same amount of time they'd have had to wait for all the data anyway.
Yes. This is what I'm saying. GQL allows for a combinatoric explosion of potentially required queries (and in extreme cases, data providers) to fufill any request. And every GQL endpoint needs to be able to service all of them unless your request routing proxy can peek into body contents, which is more expensive than URL routing.
> REST endpoints tend to force you to make multiple queries to go back and fetch information about the entities whose IDs you received in earlier requests
A problem we can solve elegantly with HTTP/2 push using nearly identical underlying API servicing models. What's great about that approach is that it's totally transparent to the client; they just get better performance with less resources.
Instead, folks have decided to discard a lot of really positive aspects of the REST model to make a client-facing DSL realized in the server.
> In both cases, combinoratic explosions (and infinite depth) are possible
But in the classical rest case, the client is aware they're doing this, as well as the server. In the GraphQL case, we've obfuscated this and said, "We reserve the right to reject your quest for any reason, and we've also made it harder for us to service your query (unless we go back to mandating every valid query as in rest), and we've also made scaling harder because it's more difficult to factor endpoints into different scaling groups."
But hey, that DSL is great. It's like JSON without tall that predictability or syntactic validation.
I cannot see any positive outcomes to adopting graphql other than that, "Client-side developers love it". If ya'll love it so much, why not maintain it on your side via service-worker query interception?
I ask facetiously. The answer is: because that would be really hard, and we'd rather push it off to API endpoint devs. Devs who promptly put restrictions that basically render the best part of GraphQL (that it is a query language) impotent for performance reasons.
How about one HTTP request often being faster than multiple requests? How about only retrieving the payload you requested rather than all the extra data the API developers decided to expose in the endpoint – bandwidth isn't free? How about not having to add new custom bespoke API endpoints because some new part of the website just needs a few little different pieces of data that would normally require several round trips, pretty please? These are just normal everyday issues that people put up with when using REST APIs.
> A problem we can solve elegantly with HTTP/2 push using nearly identical underlying API servicing models. What's great about that approach is that it's totally transparent to the client; they just get better performance with less resources.
Shouldn't you use push when you know the client will ask for the resources? How would you know whether the client will ask for certain connected objects in this case? Would you just always be pushing every connected object over the wire?
I don't really see why you're giving such special distinction to one HTTP request vs. multiple HTTP requests. That is an arbitrary distinction to make. You shouldn't be asking "how much strain can a client put on my server with a single request? but oh, they can make multiple requests…" but "how much strain is a client going to put on my server to get all the data they need, whether it happens across one request or multiple?"
Http/2 push.
But also: would it actually be more dev hours to write out the custom RESTful queries? If you're whitelisting individual queries. What's the difference then? You've just got a more awkward, uncachable, less split-routable protocol for exactly the same data.
> Shouldn't you use push when you know the client will ask for the resources?
Yes. If I know they intend to join data in, I can push it. I can even do this somewhat speculatively based on statistical patterns in clients. I can tune those values based on real data which can be refined over time.
Right, so if you don't have the full "query", which you don't with multiple REST round-trips, then you won't push it...
> I can even do this somewhat speculatively based on statistical patterns in clients. I can tune those values based on real data which can be refined over time.
Cool, so guessing. That's exactly what I want my API's performance profile to be based on. Sounds like a lot of work man, why don't you just use GraphQL instead? ;)
Yes. But of course, GraphQL ad hoc extensions are discussing limiting this arbitrarily as well.
> Cool, so guessing. That's exactly what I want my API's performance profile to be based on.
No. For example, if I can say that a banking customer wants to see a second page of transactions 90% of the time, then I should push the next page every time. If I can say they want to see the third page of transactions 10% of the time, then it makes sense to defer the cost.
It just because a confusing and unhelpful protocol choice if you start whitelisting queries. Without the "query" part it's just a DSL for graph retreival.
Why not put that client side if it is so expressive.
How does this relate to the "I" in SOLID?
(1) In what sense is my client making GraphQL queries depending on or tied to the available schema fields it does not query? Where's the coupling?
(2) I have a REST API that doesn't use PUT, PATCH, or DELETE. So I guess every REST API is not SOLID either, because it's using a general-purpose transport with features I'm not using?
(3) Does anyone care? I'm trying to make the best end-user and development experiences. GraphQL allows that. It's an improvement.
SOLID is some principals for one type of software (object oriented). Not the principles for all types of software.
Why not move the wonderful experience client side and use service workers?
People talk a lot about one-query turnaround time, but for websites this is not nearly the same concern or gain that it is for, say, a mobile API. I'm a bit out of the mobile game this year, but AFAICT they aren't rushing to embrace gql.
Meanwhile, NYTimes:
> “Facebook developed it to provide a data source that can evolve without breaking existing code and to favor speed on low-powered and low-quality mobile devices.”
[1] https://open.nytimes.com/react-relay-and-graphql-under-the-h...
Of course I haven't, but you haven't even begun to address the architectural concerns I've raised. You keep dismissing them without direct comment, then demanding I go further and further into the specifics of your unique use case to be allowed even a high level opinion.
I'm not telling you what to do. But I keep waiting for someone to tell me one good reason why I want to go back to a monolithic API dispatch. Please, tell me. Why should _I_ do this? Why don't you also worry about the architectural regression in your API here?
Facebook has haskell developers who actually have tools to write performant GraphQL servers. One of my big debates is if I want to finalize & make public some of my work to efficiently dispatch graphQL queries.
Haxl makes this entirely possible and other people have done proofs of concept as well. If I see an amazing benefit to finding time in my schedule to do this work, I will.
Of course, most folks won't use it. But we would, and that might be enough. But my other alternative is to do the exact same kind of work in the browser under service-workers. I don't really have many arguments against this other than "well you end up making the same # of queries as before so there's only minimal full query improvement."
But if i can combine a client-side gql dispatcher with http/2 push work (which inherently using query caching), it seems better. Again, lots of work in an ecosystem I'm not terribly fond of, but I can do it.
So please consider telling me what the actual benefits are and stop appealing to "developer experience" (I don't dispute this, I just can't make a decision based on it exclusively) and "faster queries" (because I think you lose at least as much performance as you gain and your API becomes a lot less predictable).
Complexity caps: Depends on the server implementation. Sangria (Scala implementation) has it built in. For others, i'm not sure. It would be easier to add a depth cap, but a complexity cap is more useful. I think whitelisting and.or rate-limiting is the way to go if you're actually concerned about your GraphQL server being abused though.
Complexity caps: In the absence of good query scheduling this is a defense mechanism by GQL server providers. Great. But why are we doing it in the first place. This is still more complex with nearly identical outcomes to restful endpoints.
As long as you have a half-decent build pipeline and embrace static patterns on the client, you get to keep all of the benefits.
To be honest, you seem like someone who hasn't done a lot of client-side work, especially with component systems. The benefits to development speed and maintainability are significant, and nearly all the downsides can be mitigated.
If you're only approaching this from the perspective of a backend developer, i'd fully expect GraphQL to be underwhelming to you.
This isn't really graphQL though, is it? This is an ORM. The ones I work with (in Blu(cough)Python) the binding layer I use works off standard ORM stuff, and could route object retrieval out of a restful API with minimal developer work as well (AuthZ, AuthN).
> As long as you have a half-decent build pipeline and embrace static patterns on the client, you get to keep all of the benefits.
I guess I simply have no idea what the benefits are. I keep asking, people keep giving me "technical" benefits that I'm challenging and I've yet to really see any refutation to those challenges.
It keeps coming back to:
> The benefits to development speed and maintainability are significant
And okay, a query DSL is useful. I totally buy into this. In fact, I have made many query DSLs. I advocate for linguistic metaprogramming every chance I get.
But what I'm saying is that the way it's implemented (server side) is, in many ways, regressive. I think you'd get most of the same benefits from a service worker doing that arbitration and dispatch. Your query whitelist updates wouldn't require infrastructure pushes AND client pushes. You'd have better caching story (because SW would make it easier to cache both the intermediate queries and prevent refetch on the top level call).
My primary objection to GraphQL is just how immature the tooling is. And I know how to dive in and provide SDKs for solving some of these hard problems, but then we end up with the 2017 equivalent of a .cgi script and I'm just... I don't get why any architect would agree to that.
> i'd fully expect GraphQL to be underwhelming to you.
Then why push it down to me as a requirement? And why recommend outrageous "solutions" like "oh well we'll just query whitelist so you lose all your flexibility." "We implement internal complexity caps you have no way to perceive from the query language but don't worry we'll 400 your request so you know."
These seem, to me, to make your frontend experience objectively worse than restful calls.
Perhaps it was "superfluous" to say. But then, so was your post.
To me, it's like saying "somewhat unique".
The phrase "relatively optimal" doesn't make sense to me. Do you have a server that's optimal relative to some other server? That's just "optimal", no? And if it's sub-optimal relative to some other server, that's just "sub-optimal"?
Locally optimal is a known thing (it even has it's own wikipedia entry https://en.wikipedia.org/wiki/Local_optimum), but "relatively optimal" is just sloppy thinking in my view.
In practice I was able to have a GraphQL server which produced optimal downstream request waterfalls (i.e. to internal APIs) for every query that our app made in practice, and nearly every contrived query I could come up with.
So what I mean is that if you'd hand-written the code to handle those data requirements, in nearly all scenarios you wouldn't have been able to do better.
Does "optimal, nearly all the time" make you feel better?
I'm sorry, but I categorically reject the value of a query language for APIs if you turn it into something indistinguishable from a POST-body driven web framework with less cachability.
What is the point of a query language that may act across multiple sub-domains if your provider then says, "But only these queries are allowed."
AFAICT, it's a waste of time. You lose so much in the migration from a RESTful postbody API in exchange for what amounts to... well... a DSL for writing API calls that any javascripter worth their salt could have made already.
I guess I can see it sitting between a data cache — that is filled by a set of relatively large-granularity fixed queries that can have a decent cache validation mechanism — and the client UI. Maybe that’s how it’s actually implemented? But then it seems like that would often be better (at least much of the time) for that data cache to be client-side.
You do want a way to pass user filter conditions back through the data access pipeline (but not business logic where-clause conditions) all the way back to the data store, but those should still be constrained.
I don't know what I need. Server tells me I need {allthedogs}. Now I know to query for {allthedogs}... what's the point of GraphQL?
I hate jacking threads pointing out X instead of Y, but in this case it feels warranted.
The only job of the future will be AI-assisted design.
Almost like an excuse to avoid having to think about a problem. "But what about ..?" "A DEEP LEARNING AI NEURAL NETWORK WOULD TOTALLY SOLVE IT RIGHT?!"