Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungKI half beim Finden: iOS 27 schließt mehr als 100 Sicherheitslücken(21.09.2026 um 06:00 Uhr)
Sichere ProgrammierungWhat Is Rowhammer? How Can Repeated Memory Access Flip Bits in RAM?(21.09.2026 um 07:12 Uhr)
Sichere Programmierungnpm publish Ignores .gitignore: The .npmignore Override Rule(21.09.2026 um 07:15 Uhr)
Sichere ProgrammierungAphelion Editor - A free node-based video / VFX editor(21.09.2026 um 07:21 Uhr)
Sichere ProgrammierungGovernance Attack Surface Review: OKX(21.09.2026 um 07:31 Uhr)
Sichere ProgrammierungJSM Portal Request Create Property Panel Submit(21.09.2026 um 07:34 Uhr)
Reverse Engineeringsearch instructions assembly easy (X86,RISCV,AARCH64,etc)(20.09.2026 um 15:44 Uhr)
Sichere ProgrammierungKI half beim Finden: iOS 27 schließt mehr als 100 Sicherheitslücken(21.09.2026 um 06:00 Uhr)
Sichere ProgrammierungWhat Is Rowhammer? How Can Repeated Memory Access Flip Bits in RAM?(21.09.2026 um 07:12 Uhr)
Sichere Programmierungnpm publish Ignores .gitignore: The .npmignore Override Rule(21.09.2026 um 07:15 Uhr)
Sichere ProgrammierungAphelion Editor - A free node-based video / VFX editor(21.09.2026 um 07:21 Uhr)
Sichere ProgrammierungGovernance Attack Surface Review: OKX(21.09.2026 um 07:31 Uhr)
Sichere ProgrammierungJSM Portal Request Create Property Panel Submit(21.09.2026 um 07:34 Uhr)
Reverse Engineeringsearch instructions assembly easy (X86,RISCV,AARCH64,etc)(20.09.2026 um 15:44 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Why I Use Plain JavaScript Objects as Interfaces (And Validate Them at Startup)

Reagiere als Erste:r — dein Feedback zählt!

How do you enforce contracts between layers in plain JavaScript? No TypeScript, no abstract classes, no DI framework. Just Node.js.

This is the system I found. It's been running in production since last summer without a single issue.

If you're coming from the previous post in this series, I promised I'd talk about those interfaces with throw new Error('Not implemented'). Here we are.

The problem with importing everything directly

Early on, my code looked like every other Node.js project. Controllers imported Firestore directly. The customer service imported the WooCommerce API. Everything worked, but everything was glued together.

The moment I realized this was a problem: I wanted to support shops without WooCommerce. Shops that only manage data from the app, no external CMS. I searched for where WooCommerce was used and found it hardcoded all over the place.

If I wanted a shop to work without WooCommerce, I'd have to add if/else checks everywhere. That wasn't going to scale.

I needed a way to say: "this service needs something that can create customers, update them, and delete them — but it doesn't care if it's WooCommerce, Shopify, or nothing at all."

Drawing boundaries with plain objects

The solution was what Hexagonal Architecture calls "ports." In my case, they're plain JavaScript objects — not classes — with methods that throw if you don't implement them:

// modules/customers/core/ports/CustomerRepository.js

const CustomerRepositoryInterface = {
  createCustomer: async (shopId, customerData) => {
    throw new Error('Not implemented');
  },
  updateCustomer: async (shopId, customerId, updates) => {
    throw new Error('Not implemented');
  },
  findById: async (shopId, customerId) => {
    throw new Error('Not implemented');
  },
  findByEmail: async (shopId, email) => {
    throw new Error('Not implemented');
  },
  // ... 19 methods total
};

No inheritance. No abstract class. It's a contract: "this is what I expect you to know how to do."

The adapters, on the other hand, are classes — FirestoreCustomerRepository, WooCommerceCustomerAdapter. They implement the port's methods with real logic. Ports define the shape. Adapters do the work. That's the split.

I have three types of ports per entity:

  • Repository — the write side (Firestore)
  • ReadModel — the read side (Meilisearch or optimized queries)
  • CMSAdapter — the connection to the external CMS (WooCommerce, or nothing)

Each one defines its contract separately. The application service only knows the ports, never the concrete implementations.

But JavaScript won't tell you if a method is missing...

This is the real problem. You can build an adapter, implement 18 out of 19 methods, and JavaScript says nothing. Everything works great until someone calls the missing method in production.

My solution: validateImplementation(). Each port exports the list of required methods and a function that checks if an implementation has all of them:

// modules/customers/core/ports/CustomerRepository.js

const REQUIRED_METHODS = Object.keys(CustomerRepositoryInterface);

function validateImplementation(implementation) {
  const missingMethods = REQUIRED_METHODS.filter(
    method => typeof implementation[method] !== 'function'
  );

  if (missingMethods.length > 0) {
    throw new Error(
      `CustomerRepository missing methods: ${missingMethods.join(', ')}`
    );
  }
  return true;
}

Notice: REQUIRED_METHODS is derived from the interface object with Object.keys(). If I add a method to the port, it's automatically required in validation. No duplicate list to maintain — that was an early mistake I already fixed.

Now here's the important part — each adapter validates itself when Node.js loads the module:

// Bottom of FirestoreCustomerRepository.js, BEFORE module.exports

validateImplementation(FirestoreCustomerRepository.prototype);

It validates the .prototype of the class against the port's contract. Port is a plain object that defines the contract. Adapter is a class that implements it. The validation bridges the two.

If I forget a method, the app won't start. Not at 3am when a customer places an order. At startup. When I'm at my desk and can fix it in two minutes.

Dependency injection without a framework

With ports defined, application services receive their dependencies through the constructor. They don't know if there's Firestore, PostgreSQL, or a mock behind them:

// modules/customers/application/services/CustomerService.js

class CustomerService {
  constructor(dependencies) {
    this.customerRepository = dependencies.customerRepository;
    this.customerReadModel  = dependencies.customerReadModel;
    this.cmsAdapter         = dependencies.cmsAdapter;
    this.addressRepository  = dependencies.addressRepository;
    this.eventBus           = dependencies.eventBus;
    this.logger             = dependencies.logger || console;
  }
}

No Inversify. No IoC container. Just an object with dependencies.

And where does it all come together? In a factory that acts as the composition root:

// modules/customers/application/services/index.js

function createCustomerServices(dependencies) {
  const { customerRepository, customerReadModel,
          cmsAdapter, addressRepository, eventBus } = dependencies;

  const customerService = new CustomerService({
    customerRepository, customerReadModel,
    cmsAdapter, addressRepository, eventBus,
  });

  const addressService = new CustomerAddressService({
    addressRepository, customerRepository, eventBus,
  });

  return { customerService, addressService };
}

This is where you decide which concrete adapter goes to each port. The service never knows.

The Null Object that saved me

Back to the original problem: supporting shops without WooCommerce.

The bad option was adding if (shop.hasCMS) everywhere the adapter was called. The good option was creating a StandaloneCustomerAdapter — it implements the exact same port, but does nothing:

// modules/customers/infrastructure/adapters/StandaloneCustomerAdapter.js

class StandaloneCustomerAdapter {
  async syncCustomer(shopId, customer) {
    return { success: true }; // No CMS, nothing to sync
  }

  async *fetchAllCustomers(shopId) {
    // Empty generator — no external customers to fetch
  }

  getType() { return 'standalone'; }
  supports(feature) { return false; }
}

// Same validation pattern as every other adapter
validateImplementation(StandaloneCustomerAdapter.prototype);

And a factory that picks the right one based on the shop's config:

// modules/customers/infrastructure/adapters/CustomerAdapterFactory.js

function createCustomerAdapter(cmsType, shopId, options = {}) {
  switch (cmsType?.toLowerCase()) {
    case 'woocommerce':
      return getWooCommerceCustomerAdapter(shopId, options);
    case 'standalone':
    case null:
    case undefined:
      return getStandaloneCustomerAdapter(options);
    default:
      throw new Error(`Unsupported CMS type: ${cmsType}`);
  }
}

CustomerService doesn't know which adapter it gets. And it doesn't care. That's the whole point.

Tomorrow, if I want to add PrestaShop, I create a new adapter, register it in the factory, and zero application services change.

The honest part

It's not perfect. Without TypeScript you lose autocomplete and types on hover. That hurts day to day.

validateImplementation() only checks that methods exist as functions — it doesn't validate signatures, return types, or anything beyond "is the method there?" It's a basic safety net, not a type system.

Sometimes I wonder if I should have started with TypeScript from the beginning.

But for a solo dev, the simplicity of "an object with methods" outweighs full type safety. I don't have a team of 10 where someone can break a contract without realizing it. It's just me, and validating at startup is enough to catch my own mistakes.

It's been in production since last summer. Not a single missing method at runtime.

What's next

Now we have ports separating the write side from the read side, and adapters implementing them. But there's a missing piece — when I write to Firestore, how does Meilisearch find out?

The answer is the event system. And it has a two-level naming trick — domain events (ecommerce.*) and infrastructure events (firestore.*) — that solved the coupling problem in a way I didn't expect.

That's the next post.

Do you enforce interfaces at runtime, or does TypeScript give you enough confidence?

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Why I Use Plain JavaScript Objects as Interfaces (And Validate Them at Startup)

Thematisch verwandte Begriffe: Plain, JavaScript, Objects, Interfaces · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-94109 | openEQUELLA versions before 2026.1.0 contain a remote code execution vul…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick