JavaScript Proxy Pattern: Control Object Access

The Proxy pattern in JavaScript allows developers to place an intermediary layer between an application and a target object to intercept, redefine, and control fundamental operations. By wrapping an original object inside a Proxy instance configured with a handler, you can govern property access, block unauthorized mutations, validate incoming data, hide private attributes, and even revoke access dynamically. This article explains how the JavaScript Proxy object works, explores the essential traps used for access control, and demonstrates practical implementations for securing and managing data flow.

Understanding the Core Components: Target and Handler

A JavaScript Proxy consists of three main elements: the target object, the handler object, and the proxy instance itself. The syntax is:

const proxy = new Proxy(target, handler);

When an operation is executed on the proxy, JavaScript checks the handler for a matching trap. If the trap is defined, it runs; otherwise, the operation falls back directly to the target object.

Intercepting Operations with Traps

Access control relies on specific handler traps that intercept attempts to interact with the underlying data.

1. The get Trap: Regulating Read Access

The get trap executes whenever a property is read. It receives the target, the property being accessed, and the receiver (the proxy instance).

You can use the get trap to hide sensitive or private properties, redirect queries, or throw errors when unauthorized keys are requested:

const user = {
  id: 101,
  username: "johndoe",
  apiKey: "secret_live_98765"
};

const userProxy = new Proxy(user, {
  get(target, prop) {
    if (prop === "apiKey") {
      throw new Error("Access denied: Property is private.");
    }
    return prop in target ? target[prop] : undefined;
  }
});

console.log(userProxy.username); // "johndoe"
console.log(userProxy.apiKey);   // Throws Error: Access denied

2. The set Trap: Validating and Restricting Write Access

The set trap intercepts attempts to modify an existing property or add a new one. It receives the target, property, value, and receiver. The trap must return true to indicate success or false/an error to reject the change.

This trap is commonly used for data validation and creating read-only objects:

const account = {
  balance: 500
};

const accountProxy = new Proxy(account, {
  set(target, prop, value) {
    if (prop === "balance") {
      if (typeof value !== "number" || value < 0) {
        throw new TypeError("Balance must be a positive number.");
      }
    }
    target[prop] = value;
    return true;
  }
});

accountProxy.balance = 750; // Allowed
accountProxy.balance = -50; // Throws TypeError

3. The has Trap: Masking Property Existence

The has trap intercepts the in operator. This prevents external code from discovering whether a specific key exists on an object:

const profile = {
  name: "Alice",
  _internalId: "A-992"
};

const secureProfile = new Proxy(profile, {
  has(target, prop) {
    if (prop.startsWith("_")) {
      return false;
    }
    return prop in target;
  }
});

console.log("name" in secureProfile);        // true
console.log("_internalId" in secureProfile); // false

4. The deleteProperty Trap: Preventing Key Deletion

The deleteProperty trap intercepts the delete operator, protecting critical keys from removal:

const config = {
  environment: "production",
  port: 8080
};

const safeConfig = new Proxy(config, {
  deleteProperty(target, prop) {
    if (prop === "environment") {
      throw new Error(`Cannot delete immutable property: ${prop}`);
    }
    delete target[prop];
    return true;
  }
});

delete safeConfig.port;        // Allowed
delete safeConfig.environment; // Throws Error

Using Proxy.revocable for Temporary Access

JavaScript provides Proxy.revocable(target, handler), which returns both the proxy instance and a revoke function. Once revoke() is called, all further operations on the proxy immediately throw a TypeError.

This is useful for granting time-limited access or safely passing sensitive objects to third-party modules:

const sensitiveData = { token: "abc-123" };

const { proxy, revoke } = Proxy.revocable(sensitiveData, {
  get(target, prop) {
    return target[prop];
  }
});

console.log(proxy.token); // "abc-123"

// Revoke access
revoke();

console.log(proxy.token); // Throws TypeError: Cannot perform 'get' on a proxy that has been revoked

Using Reflect with Proxies

When implementing traps, standard access control patterns use the built-in Reflect API. Reflect methods mirror the proxy traps and handle the default object behavior cleanly while preserving correct this binding through the receiver argument:

const handler = {
  set(target, prop, value, receiver) {
    if (typeof value === "string") {
      value = value.trim();
    }
    return Reflect.set(target, prop, value, receiver);
  }
};

Summary of Use Cases for Access Control

  1. Data Validation: Enforce schemas and types before writing to properties.
  2. Immutability: Block set and deleteProperty operations to build deeply frozen or read-only states.
  3. Information Hiding: Use get and has traps to make private fields invisible and inaccessible.
  4. Audit and Logging: Track read and write attempts to monitor security and usage across modules.
  5. Temporary Permissions: Use Proxy.revocable to cleanly sever access to an object when a task completes.