Live data from Hacker News

How to Design Better APIs

r.bluethl.net

161–170 of 238 posts

Re: How to Design Better APIs

#161
post #4

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 cal…

Another 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.

Re: How to Design Better APIs

#162
post #64

Sometimes, I feel that we ought to have a simple protocol, on top of HTTP, to simply do remote procedure calls and throw out all this HTTP verbs crap. Every request is a http POST, with or without any body and the data transfer is in binary. So that objects can be passed back and forth between client and server. Sure, there is gRPC, but it requires another API specification (the proto files). There I said it. HTTP Ve…

So the problem with “Data transfer is in binary” is that it really requires both the source and the recipients to be running the same executable, otherwise you run into some really weird problems. If you just embrace parsing you of course don't have those problems, but that's what you are saying not to do... Another great idea is for a binary blob to begin with the program necessary to interrogate it and get your val…

> So the problem with “Data transfer is in binary” is that it really requires both the source and the recipients to be running the same executable, otherwise you run into some really weird problems.

I think you're misinterpreting "data transfer is in binary" with something like "a raw memory dump of an object in your program, without any serialisation or parsing step".

Re: How to Design Better APIs

#163
https://www.vinaysahni.com/best-practices-for-a-pragmatic-re...

Helped me get started with API design in my early career. Learning from other existing APIs helps too. such as stripe, github, shopify. Any others?

Something that We do at my current job:

* set a standard and stick with it. tweak it if needed. we even have naming standard on some of the json key. for example, use XXX_count for counting. when it doesn't make sense, use total_XXX.

* Document your API, we use postman and code review API changes too.

Re: How to Design Better APIs

#164
post #67

a) Use standardized error codes, not standardized error messages. Clients are responsible for internationalization, which includes presenting error messages in the user's language. If you document a set of error codes as an enum, the client can present a user-friendly error message in the user's language based on the error code. If there are dynamic parts of the error message, i.e. "404: There is no user with ID 1234…

Any pagination is brittle. Regardless of whether its by page, cursor or something else. You can't have equal sized pages if there is a risk that there are deletions on a previous page or at the exact index you use as a cursor etc.

The solution is usually simple: assume it doesn't matter. Write a spec for your feature and explicitly state that "In the solution we assume it doesn't matter if the same record is ocassionally reported twice or a record is missing from the pagination in some cases". Done.

Re: How to Design Better APIs

#165

Earlier quoted context omitted.

Chrome, Safari, Firefox, Edge, and Opera are all third-party clients for the HATEOAS-based API known as the World-Wide Web.

What?

This answer is correct, but lacks context. REST wasn't conceived with APIs in mind. In fact, it's an awful fit for APIs, as many of the other comments point out. Rather, REST today is a buzzword that took on a life of its own, bearing only superficial resemblance to the original ideas.

HATEOAS is a generalization of how something like a website would let a client navigate resources (through hyperlinks). It requires an intelligent agent (the user) to make sense. Without HATEOAS, according to Roy Fielding, it's not real REST. Some poor misguided API designers thought this meant they should add URL indirections to their JSON responses, making everything more painful to use for those unintelligent clients (the code that is consuming the API). Don't do this.

If you must do REST at all - which should be up for debate - you should keep it simple and pragmatic. Your users will not applaud you for exhausting HTTP verbs and status codes. The designers of HTTP did not think of your API. You will likely end up adding extra information in the response body, which means I end up with two levels (status code and response) of matching your response to whatever I need to do.

If something doesn't quite fit and it looks ugly or out-of-place, that's normal, because REST wasn't conceived with APIs in mind. Don't go down the rabbit hole of attempting to do "real REST". There is no pot of gold waiting for you, just pointless debates and annoyed users.

Re: How to Design Better APIs

#166

Sometimes, I feel that we ought to have a simple protocol, on top of HTTP, to simply do remote procedure calls and throw out all this HTTP verbs crap. Every request is a http POST, with or without any body and the data transfer is in binary. So that objects can be passed back and forth between client and server. Sure, there is gRPC, but it requires another API specification (the proto files). There I said it. HTTP Ve…

gRPC is encoding agnostic, and requires _no_ Protobuf at all. See: https://grpc.io/blog/grpc-with-json/

In practice though, the tooling is cumbersome enough that you can't readily sub in some other protocol besides protobuf, json, and allegedly flatbuf. I've had little success finding ways to e.g. use msgpack as the serde. Maybe it's out there but I haven't found it.

Re: How to Design Better APIs

#167

Earlier quoted context omitted.

b -> What happens if the "list of IDs" is 10k or 100k or 1M+?

There's typically business-logic ways to require a range to be specified, and then you specify a maximum range. For example, if the resource in question is audit events, you require the user to specify a time range, up to a maximum of a day, or seven days, or whatever performance / budget constraints allow for.

So if I specify a range and something changes between in that range while I'm interacting with those, sounds like same problem is back? Just same issue; different package :)

If I do a search, a paginated search result can have an ID so I can paginate between the data without a new data messing up my pagination.

But for normal entities, simple pagination is (mostly) more than enough.

The solution you are describing is overkill and almost no benefit at all.

Re: How to Design Better APIs

#168

I’m gonna say it: Many rest apis are lazy and developer friendly, not consumer friendly. If you have related resources, let’s say, product and product options as two distinct endpoints: - /api/product - /api/options Then, and I want to be clear here, it is impossible for a client to perform an atomic operation on multiple distinct objects types. Let’s say the client needs to add a product with a single option or fail…

If options are children of products and not many-to-many then the only logical way to have two endpoints in the API is if the parent (Aggregate root in DDD speak) items are readable/writable as a consistent tree, while the query for child items in /api/options is a read only api.

There is nothing nonstandard about such a REST api. If a product must be associated with an option then the /product/create endpoint should only accept a product with an option already attached.

> This kind of “pure” rest api is simply convenient to the developer because they push the problem of object consistency to the client.

It's not like REST means that to be "pure" or "standard" you end up exposing all your database tables with read/write and just let callers CRUD anything they want, including creating inconsistent data?

> “the rules” of rest APIs.

The rules are very simple: the REST api should expose and enforce the consistency rules of the business logic!

Re: How to Design Better APIs

#169

Earlier quoted context omitted.

What?

The classic book on the subject is RESTful Web APIs[1], and it spends a while explaining HATEOAS by using the example of the web as we've come to expect it as the exemplar REST API using HATEOAS. I also have this essay[2] on HATEOAS in my open tabs, and it uses the example of a web browser fetching a web page. [1] https://www.oreilly.com/library/view/restful-web-apis/978144... [2] https://htmx.org/essays/hateoas/

This should come with a big warning for people looking to do real work. This is not what most REST APIs are like in practice, nor what they should be. The vast majority of REST APIs are RPC-like, because that's the pragmatic way to deal with the problem 99% of the time. The "REST" branding is just for buzzword compliance.

Re: How to Design Better APIs

#170
post #70

Can someone share how they handle versioning in their API when it comes to data model changes? For example `POST /users` now takes a required field `avatar_url` but it was not part of `v1`. Since this field is validated in the DB, merely having `v1` `v2` distinction at the API layer is not sufficient. So I was thinking we will have to either 1) disable DB validations and rely on app validations or 2) run two separate…

I'm not saying you should do it this way, this is just how our startup (still very much in the "move fast and discover product fit" stage) does it. We have separate API models (pydantic and fastapi) and DB models (sqlalchemy). Basically everything not in the original db schema ends up nullable when we first add a field. The API model handles validation.

Then if we absolutely do need a field non-null in the db, we run a backfill with either derived or dummy data. Then we can make the column non-null.

We use alembic to manage migration versions.

But we aren't even out of v0 endpoint and our stack is small enough that we have a lot of wiggle room. No idea how scalable this approach is.

The downside is maintaining separate api and db models, but the upside is decoupling things that really aren't the same. We tried an ORM which has a single model for both (Ormar) and it just wasn't mature, but also explicit conversions from wire format to db format are nice.

Post reply on HN