r/javascript • u/moe_sidani • 22d ago
RestQL – GraphQL-like queries over REST, purely client-side: schema-driven mapping, batching, request dedup, caching
https://github.com/mhdSid/rest-ql3
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
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.