At least when used correctly, but honestly I can’t see a situation where it’s easy to do otherwise for queries.
The utility is in not having to write a lot of repetitive endpoints in a traditional backend, for a large amount of endpoints.
What exactly do you mean by “query logic in the client code”?
In PostgREST, if userMessages is a table in itself, you do get an endpoint called /userMessages.
If the table is called messages and you want to get messages from a user, you can just request something like /messages?user_id=123. And if user_id must your own user_id, you can just skip passing the parameter, thanks to RLS.
If userMessages requires is a join between two tables and you don't want to let the frontend know about it, you can use a view and PostgREST will expose the view as an endpoint.
Once again, there is no "need" to formulate joins in the frontend to reap the benefits of this tool.
I don't do anything close to "formulating a join in the client" with PostgREST and I still use it to its full extent, and it does save time.
EDIT: If one wants to formulate more complex joins in the frontend, then they probably want something like Hasura instead. Once again: complex queries in the frontend is BY NO MEANS mandatory, you can still use flat GraphQL queries and db views for complex queries. PostgREST OTOH is about keeping it simple.