Node.js Development

Object-Oriented Programming: Developing an E-commerce Website, Node.js

By the Domain India teamPublished 8 min read
Knowledge base article
Contents (9 sections)

Object-oriented programming (OOP) organises code around objects that hold data and the behaviour that goes with it. An online shop is a natural fit: products, carts and orders are real things with clear rules. This tutorial teaches OOP in modern JavaScript on Node.js by building the core of an e-commerce system, and it points out where JavaScript differs from Java or C#.

Key takeaways

JavaScript classes are syntax over prototypes, but they give you everything you need: constructors, inheritance with extends and super, truly private fields with #, getters, and static members. There are no built-in interfaces, abstract classes or final; you emulate them or use TypeScript. Store money as whole paise, not floating-point rupees, prefer composition over deep inheritance, and never keep plain-text passwords in any object.

1. Set up a small Node.js project

Use a current Node.js LTS release (an even-numbered version). Create a project that uses ES modules:

bash
mkdir shop && cd shop
npm init -y
npm pkg set type=module

Each class below can live in its own file under src/ and be shared with export class ... and import { Product } from './product.js'.

2. Classes, constructors and private fields

A class is a blueprint; new creates an object from it. Fields starting with # are private: code outside the class cannot read or change them, and the language enforces it.

javascript
export class Product {
  #pricePaise;

  constructor({ sku, name, pricePaise, category }) {
    if (!Number.isInteger(pricePaise) || pricePaise < 0) {
      throw new RangeError('pricePaise must be a non-negative integer');
    }
    this.sku = sku;
    this.name = name;
    this.category = category;
    this.#pricePaise = pricePaise;
  }

  get pricePaise() {
    return this.#pricePaise;
  }

  describe() {
    return `${this.name}: ${formatRupees(this.pricePaise)}`;
  }
}

export function formatRupees(paise) {
  return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR' })
    .format(paise / 100);
}

Three habits are worth copying from this example:

  • Validate in the constructor, so an invalid object can never exist.
  • Expose data through getters when outsiders should read but not change it.
  • Store money in paise as integers. 0.1 + 0.2 is not 0.3 in JavaScript; rounding errors in prices end up on invoices. Convert to rupees only for display.
What about protected members?

JavaScript has no protected. An underscore prefix such as _stock is only a convention and hides nothing. Use #private fields for data that must stay inside the class, and a getter or method for anything subclasses need.

3. Inheritance and polymorphism

extends creates a subclass; super calls the parent's constructor or methods. A subclass can override a method, and code that calls describe() works with any product type. That is polymorphism.

javascript
export class ClothingItem extends Product {
  constructor({ size, colour, ...rest }) {
    super(rest);
    this.size = size;
    this.colour = colour;
  }

  describe() {
    return `${super.describe()} (Size ${this.size}, ${this.colour})`;
  }
}

export class Book extends Product {
  constructor({ author, isbn, ...rest }) {
    super(rest);
    this.author = author;
    this.isbn = isbn;
  }

  describe() {
    return `${super.describe()} by ${this.author}`;
  }
}

In a subclass constructor, call super() before you touch this, or JavaScript throws a ReferenceError. JavaScript has no method overloading by parameter list; one method checks its arguments instead.

4. Static members, abstract classes and interfaces

Static members belong to the class, not to each object. They suit factory methods and shared constants:

javascript
export class Order {
  static STATUSES = ['new', 'paid', 'shipped', 'delivered', 'cancelled'];
  static fromCart(cart, customerId) {
    return new Order(customerId, cart.lines(), cart.totalPaise());
  }
  // constructor and methods in section 6
}

JavaScript has no abstract keyword, but new.target lets a base class refuse direct use:

javascript
export class PaymentMethod {
  constructor() {
    if (new.target === PaymentMethod) {
      throw new TypeError('PaymentMethod is abstract; use a subclass');
    }
  }
  async charge(order) {
    throw new Error(`${this.constructor.name} must implement charge()`);
  }
}

There are no interfaces either. JavaScript uses duck typing: any object with a charge() method can be used as a payment method. If you want interfaces, abstract and compile-time checks, use TypeScript, which adds all three and compiles to the same JavaScript.

5. Composition and mixins

Deep inheritance trees become rigid. A "discounted clothing item that is also a gift" does not fit one parent class. Composition, giving an object the parts it needs, is usually cleaner:

javascript
export class PercentOff {
  constructor(percent) { this.percent = percent; }
  apply(paise) { return Math.round(paise * (100 - this.percent) / 100); }
}

export class PricedLine {
  constructor(product, quantity, discount = null) {
    this.product = product;
    this.quantity = quantity;
    this.discount = discount;
  }
  totalPaise() {
    const unit = this.discount
      ? this.discount.apply(this.product.pricePaise)
      : this.product.pricePaise;
    return unit * this.quantity;
  }
}

A mixin is a function that takes a class and returns an extended class, such as const Reviewable = (Base) => class extends Base { ... }. Mixins share behaviour across unrelated classes, but use them sparingly; composition is easier to follow and test.

6. The cart and the order

The cart holds lines keyed by SKU and knows its total. The order copies the lines at checkout and controls which status changes are allowed:

javascript
export class Cart {
  #lines = new Map();

  add(product, quantity = 1, discount = null) {
    const existing = this.#lines.get(product.sku);
    const qty = (existing?.quantity ?? 0) + quantity;
    this.#lines.set(product.sku, new PricedLine(product, qty, discount));
  }

  remove(sku) { this.#lines.delete(sku); }
  lines() { return [...this.#lines.values()]; }
  totalPaise() {
    return this.lines().reduce((sum, line) => sum + line.totalPaise(), 0);
  }
}

const NEXT = { new: ['paid', 'cancelled'], paid: ['shipped', 'cancelled'], shipped: ['delivered'] };

export class Order {
  #status = 'new';

  constructor(customerId, lines, totalPaise) {
    this.customerId = customerId;
    this.lines = Object.freeze([...lines]);
    this.totalPaise = totalPaise;
  }

  get status() { return this.#status; }

  moveTo(status) {
    if (!(NEXT[this.#status] ?? []).includes(status)) {
      throw new Error(`Cannot move order from ${this.#status} to ${status}`);
    }
    this.#status = status;
  }
}

Try it:

javascript
const shirt = new ClothingItem({ sku: 'SH-01-M-BLU', name: 'Cotton shirt',
  pricePaise: 79900, category: 'Men', size: 'M', colour: 'Blue' });
const cart = new Cart();
cart.add(shirt, 2, new PercentOff(10));
const order = Order.fromCart(cart, 'cust_42');
order.moveTo('paid');
console.log(formatRupees(order.totalPaise), order.status);

Because the status is private and every change goes through moveTo(), no code anywhere can ship an unpaid order by accident. That is what encapsulation buys you.

Never model users with plain-text passwords

Older tutorials store a password inside a User object and compare strings. Never do that. Hash passwords with bcrypt or Argon2 when the user signs up, store only the hash in the database, and compare with the library's verify function. Better still, use a proven authentication library or provider.

7. From model to real shop

These classes are the domain model: the rules of your shop, independent of any framework. To make a working site:

  • Persist products, carts and orders in a database such as PostgreSQL or MySQL, through a repository class, so the model does not depend on SQL.
  • Expose an API with a framework such as Express or Fastify, keeping route handlers thin and calling the model.
  • Take payments through a gateway's official SDK, and treat its webhook, not the browser, as proof of payment.
  • Test the model with Node's built-in test runner (node --test), since pure classes need no server to test.

8. Running this on Domain India

  • Shared hosting (cPanel, DirectAdmin). Run a small Node.js app through the panel's Node.js app tool; on cPanel, Setup Node.js App offers Node.js 20, 22 and 24. See deploy a Node.js app on shared hosting.
  • App Platform. Node.js apps are detected from package.json and built automatically, and every plan includes PostgreSQL. See App Platform: getting started.
  • VPS. Full root access for your own Node.js, database and process manager setup. The VPS is self-managed.
App Starter
₹100/mo + GST
  • 512 MB RAM per app
  • 1 vCPU
  • 5 GB NVMe SSD
  • PostgreSQL Database
See plan details

Prices on the card are Domain India list prices and exclude 18% GST.

Frequently asked questions

Does JavaScript have real classes?

JavaScript has class syntax, added in ES2015, but it works on top of prototypes. Classes give you constructors, inheritance with extends and super, private fields and static members, which is enough for object-oriented design.

How do I make a private property in a Node.js class?

Prefix the field name with a hash, such as #price. The language blocks access from outside the class. An underscore prefix is only a naming convention and does not hide anything.

Does JavaScript support abstract classes and interfaces?

Not natively. You can block direct use of a base class by checking new.target in its constructor, and rely on duck typing for interfaces. TypeScript adds real abstract classes and interfaces with compile-time checks.

Why store prices in paise instead of rupees?

JavaScript numbers are floating point, so decimal amounts pick up rounding errors, for example 0.1 + 0.2 gives 0.30000000000000004. Storing whole paise as integers keeps totals exact; divide by 100 only when you display a price.

Should I use inheritance or composition?

Use inheritance for a true "is a" relationship with a shallow hierarchy, such as a Book being a Product. For behaviour you want to mix and match, such as discounts or gift wrapping, composition is more flexible and easier to test.

Where can I host a Node.js shop backend?

On Domain India, small apps can run on cPanel or DirectAdmin shared hosting through the Node.js app tool, the App Platform builds Node.js apps automatically with PostgreSQL included, and a VPS gives full control.

Ready to deploy your Node.js app? Start on the App Platform, or compare VPS plans if you want full control.

Deploy your Node.js app

Node.js apps are detected and built automatically, with PostgreSQL and automatic SSL included on every plan.

See App Platform

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app