Hacker News new | ask | show | jobs
Show HN: LazyQL: Only compute the GraphQL fields requested (TypeScript) (github.com)
1 points by matutetandil 105 days ago
Hey HN, I built LazyQL, a small TypeScript library, because I was tired of GraphQL resolvers doing unnecessary work.

The problem: In a typical GraphQL API, your resolver computes all fields for an object, even if the client only asked for 2 out of 15. This means unnecessary database calls, API requests, and CPU time — all thrown away.

The solution: LazyQL uses JavaScript Proxy to intercept field access. You write a class with getter methods (getStatus(), getCustomerEmail(), etc.), and LazyQL ensures each getter only runs when GraphQL actually reads that field.

  @LazyQL(OrderDTO)
  class Order {
    constructor(private id: number, private db: Database) {}

    getEntityId() { return this.id; }
    getStatus() { return this.db.getOrderStatus(this.id); }
    async getCustomerEmail() { return this.db.getCustomerEmail(this.id); }
    async getShippingAddress() { return this.db.getShippingAddress(this.id); }
  }

  // Query { entity_id, status } → only getEntityId() and getStatus() run
  // getCustomerEmail() and getShippingAddress() never execute
In my benchmarks, a query requesting 3 out of 10 fields made 6 calls instead of 35 with the traditional approach.

Key design decisions: - Works transparently with Apollo, Mercurius, or any GraphQL server — they don't know LazyQL exists - Naming convention maps snake_case DTO fields to getCamelCase methods automatically - @Shared() decorator caches expensive operations that multiple getters depend on - Validates at startup that all required DTO fields have matching getters (fail fast) - Zero runtime dependencies beyond reflect-metadata

It's been running in production for a few weeks now with zero issues. The whole point was to build something invisible — it sits there, does its job, and doesn't interfere with anything.

~400 lines of code, MIT licensed. Would love to hear your thoughts.

2 comments

Isn't that equal to just returning `{ typename: "MyTypeName", id: 123 }` from a resolver and then `graphql-js` just running the field resolvers on the `MyTypeName` type as needed?

Or is this doing more?

You're right that the core idea is the same — field resolvers in graphql-js already give you lazy resolution per field. LazyQL doesn't reinvent that mechanism; it sits on top of it.

The difference is developer experience:

With plain field resolvers, you'd write something like:

  // Scattered across your resolver map
  MyTypeName: {
    status: (parent) => db.getOrderStatus(parent.id),
    customer_email: (parent) => db.getCustomerEmail(parent.id),
    shipping_address: (parent) => db.getShippingAddress(parent.id),
    // ...15 more fields
  }

  With LazyQL, everything lives in a single class with shared state:

  @LazyQL(OrderDTO)
  class Order {
    constructor(private id: number, private db: Database) {}

    getStatus() { return this.db.getOrderStatus(this.id); }

    getGrandTotal() {
      return this.getOrderDetails().grand_total;
    }

    getCurrencyCode() {
      return this.getOrderDetails().currency_code;
    }

    @Shared()
    getOrderDetails() {
      // Called by two getters, but executes only once
      return this.db.getFullOrder(this.id);
    }
  }
The main wins: @Shared() caching across getters, startup validation (missing a getter = immediate error, not a silent null at runtime), and keeping all the logic for a type in one place instead of scattered field resolvers.

So it's not doing something fundamentally different — it's a pattern for organizing it better.

nice