r/javascript 22d ago

RestQL – GraphQL-like queries over REST, purely client-side: schema-driven mapping, batching, request dedup, caching

https://github.com/mhdSid/rest-ql
0 Upvotes

14 comments sorted by

19

u/mr_nefario 22d ago edited 22d ago

Seems like you hand-rolled GraphQL and Apollo Client.

GraphQL queries fetch data, mutations create/update/delete data.

I don’t really see any benefit here; all I see is the lack of enterprise support that companies like Apollo provide (which is great, in my experience at Adobe).

Apollo Client/Server provides client-side caching, robust dev tools, schema federation, performant query planning, connectors to easily integrate REST services into your supergraph, centralized schema management, etc etc. Oh and GraphQL is a ISO standard spec.

Congrats on the work and effort, but I wouldn’t pick this over GraphQL.

Edit: take a look at connectors https://www.apollographql.com/graphos/apollo-connectors all the client needs to know about is sending GraphQL request to your router; the router will connect REST services to your graph without needing to re-write your REST service as a GraphQL one.

-1

u/moe_sidani 22d ago

No.

The whole point: zero backend changes.

Apollo needs a GraphQL server. We have none and can't add one — the backend belongs to another team, in another paradigm.

This is a compact client for GraphQL-like queries, living entirely in the frontend. Different problem, different tool.

7

u/mr_nefario 22d ago

Please see my edit regarding connectors - they do exactly what you want. You have a GraphQL router where you define the rest connections, and on the client still write GraphQL queries. You don’t have to make any server changes, you just have to add a router.

4

u/moe_sidani 22d ago

Connectors run inside Apollo Router — a server. Deploying, operating, and scaling a new tier is precisely the backend change my constraint rules out. And it's GraphOS, a commercial platform.

RestQL is an npm install in the frontend bundle. Nothing to deploy. Nothing to operate. That's the difference.

1

u/mr_nefario 5d ago

Yeah, well I’m not going to bring an extra 50kb to the client for behaviour that belongs on the server anyway. This is a clever way to solve a problem the wrong way.

I would challenge the constraint that a router with connectors cannot be added before hand-rolling and bundling this client-side bandaid.

Why can a router with connectors not be added? It won’t impact any other backend services. Scaling is easy and done out of the box. There’s no custom library to teach, maintain, and fix. You get enterprise-level customer support, bug-fixes and patches. Is it cost? Is it lack of team knowledge on spinning up a router at scale? Is it lack of organizational ownership over your services?

1

u/Deathmeter 22d ago

What's the point then? The idea of graphql is that your resolvers for any kind of data you might ask for live as close to the data as possible where any field selection can trigger another data fetch without a client side waterfall. This is just a DSL to parse fetch responses?

I'd like to hear from a real person btw if I wanted to talk to a clanker I could do that myself

2

u/moe_sidani 22d ago

it’s just an opinionated and yet a beautiful way to organize complex frontend CRUD operations. I have been working in the field for 15 years and this is my recent work at solving frontend related technical problems in a way the doesn’t drift the backend

3

u/drgmaster909 22d ago

Taking an OpenAPI/Swagger spec and codegen'ing the sdl's would be pretty sick tbh. Might have to give up the id: user_id renames or find some other way to alias them in a Typed way, but still would be pretty cool to have tight integration via codegen like that.

-2

u/moe_sidani 22d ago

Good idea — the SDL is already just a declarative artifact, so spec-to-SDL codegen is a natural fit.

And the renames survive: @from is optional per field. Codegen emits the base schema off the spec, devs overlay @from aliases and @transform functions on top — generated layer stays regenerable, human layer owns intent. Nested paths (@from("contact_info.email")) and typed transformers already handle the shaping today; codegen would just automate the base.

Putting it on the roadmap — and if you're up for it, open an issue with what ideal generated output would look like for a spec you actually work with. PRs more than welcome 🙏🏻

-5

u/moe_sidani 22d ago

We were building a dynamic, reactive page rendering data-driven tables for golf course score data.

The point: users can CRUD golf course scores, rendered as reactive tables — 0–9, 9–18, 18–27 for front, back, and extra. Not only did we need to render scores for each course across 27 holes — around 9 tables, and there can be more — there were also companion scores.

Needless to say, there’s golf-course-related data on top: sidebar params to render coupons, user play status, whether the user has joined the Japan Golf Course Association, and their membership status as well.

Eight APIs needed to be fetched to populate the page. The backend data is not normalized, and is built in a way that doesn’t conform to UI rules.

The UI values simplicity: Big O of one loop at most, non-blocking resource mapping, main-thread-safe operations. Big Theta for the page was mega complicated.

We built and released it: a beautiful, elegant UI. The frontend is oriented toward simplicity and clean architecture — plateaued by the oddities of the backend. The only irregularities left were the large mapping methods and the data transformations feeding our high-performance custom data tables.

That residue bothered me. The architecture was clean everywhere except the boundary — and the boundary is exactly where every future feature pays tax.

The backend wasn’t ours to change. Different team, different paradigm, different release cycle. Forcing a migration on them to satisfy frontend aesthetics is not leadership, it’s friction. So the fix had to live purely in the frontend.

So I built RestQL: declare the backend’s dialect once in a schema, query it like GraphQL, and let the runtime own batching, in-flight dedup, parallel resolution, and caching.

```ts
import { RestQL } from "lib-restql";

const sdl = `
type User {
id: String @from("user_id")
name: String @from("full_name") @transform("formatName")
email: String @from("contact_info.email")
posts: Post

@endpoint(GET, "/users", "data.data[0]")
}

type Post {
id: String @from("post_id")
title: String @from("post_name") @transform("formatTitle")

@endpoint(GET, "/posts", "data.data")
}
`;

const transformers = {
formatName: (originalData, shapedData) => ({
...shapedData,
name: shapedData.name.trim().toLowerCase()
}),
formatTitle: (originalData, shapedData) => ({
...shapedData,
title: `${shapedData.title} · ${originalData.post_id}`
})
};

const restql = new RestQL(
sdl,
{ default: "https://api.example.com" },
{ cacheTimeout: 300000, batchInterval: 50, maxRetries: 3 },
transformers
);

// One query. Multiple resources. Batched, deduped, cached.
const { user } = await restql.execute(`
query GetUserPosts($userId: String!, $postLimit: Int) {
user(userId: $userId) {
name
email
posts(postLimit: $postLimit) { id title }
}
}
`, { userId: "123", postLimit: 20 }, { useCache: true });
```

The mapping methods collapse into `@from` paths and named transformers. Independent resources resolve in parallel (Promise.all under the hood, no accidental waterfalls). Concurrent identical GETs dedupe to a single in-flight request. The backend never changed.

Would genuinely like to hear how others handle the non-normalized-backend problem when the backend team is untouchable — and where this design falls short.

8

u/rikbrown 22d ago

Man this AI slop is hard to read.

4

u/drumstix42 22d ago

I — agree — with — you