6 ms·
Some nice tips in here. However, tip 15, I strongly disagree with: > 15. Allow expanding resources I would suggest the opposite. A REST API should not return
by pan69 5y ago
Some nice tips in here. However, tip 15, I strongly disagree with:
> 15. Allow expanding resources
I would suggest the opposite. A REST API should not return nested resources at all. Instead, and to stay with the example provided on the website, to obtain the "orders", the /users/:id/orders endpoint should be called.
It might be tempting to return nested resources, because clients would only have to make a single call.Technically this is true but once the domain of your API starts to grow, you will find that the interface will become increasingly muddled.
The suggestion provided (use query parameters) is basically a work-around. If you want to offer this to your clients, front your REST API with a GraphQL API instead. It is literally the problem that GraphQL solves. Keep your REST API clean and dumb and focused around CRUD.
- metadat 5y agoDear PAM69, this is great advice if you want to end up with a slow-to-load, low-performing web application that your customers complain about and hate using. But at least it'll adhere to a specific notion of architectural "purity", right? /s Anytime clients need to make 15 async calls before the UI can be displayed, you're headed up the creek. Generally speaking, this is an anti-pattern. There are exceptions, but they're not the rule. It's better to weigh the tradeoffs in any given situation and make a decision about bundling sub-resource references based on what you're optimizing for. A few quick examples: * Dev speed: Unbundled * Quick-loading UI: Bundled * Sub-resources are computationally-expensive to query? Unbundled This has been my experience consistently throughout 20 years of web-tier development.
- BulgarianIdiot 5y agoWell they said use GraphQL, which is a solid way to avoid REST's clumsiness once and for all.
- nerdponx 5y agoSort of, but is it any better if the GraphQL layer still has to make 15 requests in order to serve a single useful response?
- pan69 5y agoBut those 15 requests are then all occurring over a local network (in the data center), not over the Internet. The true power with GraphQL is that it might not even make all 15 calls because it will entirely depend on what you are querying for. E.g. if you query for a User but not the Orders for that User, then the request to retrieve the orders is simply skipped by GraphQL.
- doctor_eval 5y agoAlso, of course, those 15 calls are occurring in parallel. I love how GraphQL makes all the complexity of marshalling data go away. Even when a GraphQL server is directly fronting an SQL database, I found the latency to be better than what I'd probably get if I was to code the calls manually.
- nerdponx 5y agoGreat point. I have only used Apollo for GraphQL, and I found a few things about it offputting (e.g. I need a 3rd-party library to figure out what fields the client actually requested). What GraphQL server do you use? Or is Apollo + Express generally a good "default" option for basic setups?
- RedShift1 5y agoI used Apollo server as my first GraphQL implementation but found it really cumbersome. Plus Javascript fatigue hasn't caught up with the Apollo team yet, they like to change things around every so often. Now I use Postgraphile which basically creates the API for you based on a PostgreSQL database and extend it with plugins for custom operations. Hasura is also a good option but harder to extend.
- pan69 5y agoLike I suggested, use GraphQL to solve this problem. I know front-end teams that run their own GraphQL server to abstract away clumsy APIs and to optimize client/server requests.
- doctor_eval 5y agoI think this is an unnecessarily harsh and sarcastic tone to take here. The comment you're replying to set out specific reasons why they disagree with expanding/bundling sub-resources. It obviously depends on your use case - and in fact they say use GraphQL, which I heartily agree with - but the point is that you don't always know how the API is going to evolve over time, and keeping things unbundled tends to be a "no regrets" path, while bundling resources by default, in my experience, can lead to trouble later. When the API evolves - as it probably will - using bundled resources ends up running the risk of either an inconsistent API (where some stuff is bundled and some isn't, and the consumer has to work out which), a slow API (because over time the bundled data becomes a performance burden), or a complicated API (where you need to embed multiple, backward-compatible options for bundling and/or pagination in a single operation). In addition, the bundling of resources commits you to I/O and backend resource consumption based only on an assumption that the data is required. None of this makes sense to me. In practice, if you can keep your API latency reasonably low and take a little bit of care on the client side, there's no reason a user should notice the few milliseconds of additional latency caused by a couple more API calls during the page draw of an app. It's not about architectural purity, it's about decomposing your application in a way that balances multiple conflicting needs. I agree with pan69, after many years of doing this in multiple contexts, my default has become to not bundle resources when responding to an API request.
- arnorhs 5y agoI agree. One thing to add, is that there's nothing preventing you to invent a new noun for a particular rest resource that returns bundles of content. eg. /user/:id/dashboard - it makes it so this endpoint is not tied to eg. user/:id .. making that endpoint harder to change in the future, but also solves the issue mentioned by the rude comment above re: needing to perform a lot of separate rest calls.
- JaggedJax 5y agoI've consumed the kinds of APIs you're referencing and they are my least favorite. I would prefer a poorly documented API over one that returns me 15 IDs that I must look up in separate calls. I think there's a reason those APIs also tend to have rate limits that are way too low to be useful.
- iratewizard 5y ago
- runarberg 5y agoI would rather you not assigning hate to the entirety of autistic developers. There are plenty of autistic developers who are fully capable of designing great APIs with awesome usability. Being autistic has nothing to do with how APIs are developed.
- iratewizard 5y ago
- synthmeat 5y agoWe're querying dozens, if not into hundreds, of GraphQL APIs for a couple of years now. Terrible DX, terrible performance, terrible uptimes. Everyone on the team, with no exception, hates them. Even a lot of those who produce them cobble together a bad REST implementation for parts of their own products. Agreed even at rate limits comment - frequently, probably in moments of desperation, they rate limit according to Host header to keep their own products up and working.
- RedShift1 5y ago> Terrible DX Can you elaborate what's terrible about the developer experience? If anything it's much better than REST, even if the developer of the API doesn't bother with documentation, the GraphQL schema is fully usable as documentation. Plus the way to input and output data into the API is standardized and the same across all GraphQL API's. > terrible performance How? Is the API slow to respond? Or are you making too many calls (which you shouldn't do) increasing the round trip time? > terrible uptimes I fail to see how that uptime is related to GraphQL. REST API's can be just as unreliable, if the server is down, the server is down and no technology can fix that.
- 8note 5y agoThat's not a rest-y API. You want GET /orders?user=Id Orders can be searched on many dimensions, not inherent to users
- pan69 5y agoTrue. But it sort of depends on the kind of relationship "orders" has. E.g. "orders" is a good example to use your suggested GET /orders?user_id=:id and that is probably because an order has a many-to-many relationship, e.g. with users and products (i.e. an order doesn't belong to neither user nor product). However, take something like an "address" which might belong to a user (one-to-many), i.e. the user has ownership of the relationship with an address, in that scenario you probably want to use GET /users/:id/addresses But then again, when it comes to API's, domain modelling is the hard part and it is therefore the reason why you don't want to return nested results/objects.
- CipherThrowaway 5y ago> you will find that the interface will become increasingly muddled. Will I? For example The Stripe API uses expand parameters and I prefer that approach to "atomic" REST or being forced to use GraphQL. There is a missed standardization opportunity for incremental graph APIs built on REST.
- magicalhippo 5y ago> to obtain the "orders", the /users/:id/orders endpoint should be called Ok, but an order has an array of order lines, and each order line has sub-arrays as well, like details of the packages it was shipped in etc. So that might be 5-10 calls to get an order line, and for a few thousand lines we're looking at several tens of thousand calls to get a full order. Secondly, you then make some changes and want to replace the order with the data you got. You then have to produce a delta and upload that. And those thousands of calls better be done in a transaction of sorts, otherwise you'll have big issues. Seems easier to me to just be able to GET or PUT an entire order with all the details in one go.
- BeefWellington 5y ago> Ok, but an order has an array of order lines, and each order line has sub-arrays as well, like details of the packages it was shipped in etc. > So that might be 5-10 calls to get an order line, and for a few thousand lines we're looking at several tens of thousand calls to get a full order. You definitely need to pick and choose the granularity you offer. There's a level of normalization in data structuring that approaches absurdity and this would be a good example of an absurd case. It is however an excellent demonstration of why making overbroad "you must never do X" rules is dangerous. I think a decent test is to ask yourself the question: "How much data is here that I don't need?" In the example referenced, if I'm looking at the full order history, I probably want to see only summary/header data with the option to view the full order details, so it wouldn't make sense to return to the user potentially a hundred thousand order lines' worth of data just because they're trying to view their history. If I then want to view the /orders/:id for one specific order, at that point it does make sense to return all of the related lines, shipment details, etc.
- Too 5y agoOnce your api query parameters reach a certain level of complexity, like such nested lookups, one should just consider giving direct proxied read access to the DB for clients that need it. Why reinvent your own query language. I Don’t know enough about graphql to compare it. Databases have access controls also.
- berkes 5y agoAnother two reasons to avoid nested resources, are performance and coupling. A nested resource is hard to optimize. Simple, atomic, flat resources can be cached (by clients, proxies or server) much more efficient. Once you allow nested resources, there's no going back, as clients will depend on them. So you've effectively disabled lots of performance improvement options. A nested resource implies data relations. Tightly coupled to a data model. One that will change over time, yet the api is hard to change. If you have a Project nested in your Users, and the business now needs multiple projects per user, this is hard to change with nested resources, but much easier with endpoints, the /users/:id/project can be kept and return e.g. the first project, next to a new /users/:id/projects.
- berkes 5y agoAnd a third one is more architectural in nature. Your domain has bounded context (even if you haven't defined these explicitely). Within those bounded contexts, you'll have atomicity-constraints: transaction boundaries. Those should typically align, but are hard to find in a new project and will grow and shift over time. To take that User/Project example: the Project and User have different transaction boundaries: it matters not that a User.username is updated while a Project.projectname is updated differently. But it does matter when a username and password get updated simultanous. And -for the sake of the xample- it matters that the user + users-roles must be within one transaction boundary. By allowing nested resources on mutations you essentially break the bounded contexts and the transaction boundaries. By not allowing nested resources on mutations, but allowing them on-read, you introduce inconsistencies: the resources on-insert differ from the resources on-read. This becomes even more problematic when you do allow certain nested resources on mutations but not on others (because, behind the scenes you've determined a bounded context/transaction boundary). To an API consumer this is entirely arbitrary. Why can I "PUT user.roles[1]" but not "PUT user.projects[1]", yet have both included at wish on-read? With nested resources, here too, you paint yourself in corners. They take away a lot of choices you likely want to make in future. Being a bit more restraining in what you allow clients to do, keeps those options open for you. In this case, keeps the option to move transaction boundaries when business needs require (and they will).