TL;DR: A class is a blueprint for creating objects with the same structure and behavior. You define it once with properties (data) and methods (functions), then create as many instances as you need. The constructor runs when you use new. this refers to the specific instance. Classes appear in AI-generated code for services, error types, and data models — understanding them unlocks a large chunk of Node.js and TypeScript patterns.

Why AI Coders Need to Know This

Classes are not the most common pattern in modern React frontends — hooks and functional components dominate there. But in the rest of the JavaScript ecosystem, classes are everywhere. AI tools use them when generating:

  • Custom error typesclass ValidationError extends Error
  • Service layersclass EmailService { send() {...} }
  • Data models — Mongoose schemas, TypeORM entities
  • Backend frameworks — NestJS is entirely class-based with decorators
  • Design patterns — Singleton, Repository, Factory patterns
  • SDK wrappers — Many third-party SDKs export classes

You do not need to write classes from scratch to work with them. But when AI generates class-based code, you need to understand what a constructor does, why this matters, what extends means, and how to instantiate and call methods on a class. Without that, you are working blind in a significant portion of the Node.js codebase.

The Core Idea: Blueprint vs. Instance

Think of a class as a blueprint for a house. The blueprint describes how to build a house: how many rooms, where the doors go, what materials to use. The actual house is an instance — a specific house built from that blueprint. You can build many houses from the same blueprint, and each one has its own rooms, furniture, and residents.

In code:

// The class is the blueprint
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  greet() {
    return `Hello, I'm ${this.name}`;
  }
}

// Instances are specific objects created from the blueprint
const chuck = new User('Chuck', 'chuck@example.com');
const alice = new User('Alice', 'alice@example.com');

console.log(chuck.name);    // 'Chuck'
console.log(alice.name);    // 'Alice'
console.log(chuck.greet()); // "Hello, I'm Chuck"
console.log(alice.greet()); // "Hello, I'm Alice"

One class definition. Two independent instances. Each has its own name and email, but both share the same greet method logic.

Real Scenario

You are building an API with several external service integrations — email, SMS, and push notifications. You ask Claude Code to structure the notification layer cleanly.

Prompt I Would Type

Create a notification service for my Node.js app:
- EmailNotification, SMSNotification, and PushNotification services
- Each should have a send(recipient, message) method
- They should share a base class with common validation logic
- Add proper error handling with a custom NotificationError class
- Show me how to instantiate and use them

What AI Generated

// Custom error class — extends built-in Error
class NotificationError extends Error {
  constructor(message, service, recipient) {
    super(message);               // Call Error's constructor
    this.name = 'NotificationError';
    this.service = service;
    this.recipient = recipient;
    this.timestamp = new Date();
  }
}

// Base class with shared logic
class BaseNotification {
  constructor(serviceName) {
    this.serviceName = serviceName;
  }

  // Shared validation — any subclass can call this
  validate(recipient, message) {
    if (!recipient) throw new NotificationError('Recipient required', this.serviceName, recipient);
    if (!message) throw new NotificationError('Message required', this.serviceName, recipient);
    if (message.length > 1000) throw new NotificationError('Message too long', this.serviceName, recipient);
  }

  // Abstract method — subclasses MUST override this
  async send(recipient, message) {
    throw new Error(`${this.serviceName} must implement send()`);
  }
}

// Subclass — inherits from BaseNotification
class EmailNotification extends BaseNotification {
  constructor(apiKey) {
    super('Email');             // Calls BaseNotification's constructor
    this.apiKey = apiKey;
    this.from = 'noreply@myapp.com';
  }

  async send(recipient, message) {
    this.validate(recipient, message);  // Inherited from base class

    // Email-specific sending logic
    console.log(`Sending email to ${recipient}: ${message}`);
    // await emailProvider.send({ from: this.from, to: recipient, body: message });
    return { success: true, service: 'email', recipient };
  }
}

class SMSNotification extends BaseNotification {
  constructor(twilioSid, twilioToken) {
    super('SMS');
    this.sid = twilioSid;
    this.token = twilioToken;
  }

  async send(recipient, message) {
    this.validate(recipient, message);

    if (message.length > 160) {
      message = message.substring(0, 157) + '...'; // Truncate for SMS
    }

    console.log(`Sending SMS to ${recipient}: ${message}`);
    return { success: true, service: 'sms', recipient };
  }
}

// Usage
const emailService = new EmailNotification(process.env.EMAIL_API_KEY);
const smsService = new SMSNotification(process.env.TWILIO_SID, process.env.TWILIO_TOKEN);

await emailService.send('chuck@example.com', 'Your order shipped!');
await smsService.send('+12083334444', 'Your order shipped!');

Understanding Each Part

constructor()

The constructor is a special method that runs automatically when you create a new instance with new. It receives the arguments you pass to new and sets up the object's initial state by assigning values to this properties.

const emailService = new EmailNotification(process.env.EMAIL_API_KEY);
//                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
//                                          This becomes `apiKey` in the constructor

If a class has no constructor defined, JavaScript provides a default empty one. For subclasses (classes using extends), you must call super() before using this in the constructor — this runs the parent class's constructor first.

this keyword

this inside a class always refers to the specific instance — the particular object being operated on. this.apiKey in EmailNotification means "the apiKey that belongs to this specific email service instance." Different instances have their own this, so they do not share state.

The most common this bug: passing a class method as a callback causes this to lose its binding:

const service = new EmailNotification(key);

// ❌ 'this' is lost when passed as a callback
setTimeout(service.send, 1000);

// ✅ Bind this explicitly, or use an arrow function wrapper
setTimeout(() => service.send('chuck@example.com', 'hello'), 1000);

extends and super()

extends creates a subclass that inherits everything from the parent class. EmailNotification extends BaseNotification means:

  • EmailNotification gets all of BaseNotification's methods (validate, the base send)
  • EmailNotification can override those methods with its own versions
  • EmailNotification can add new methods specific to email

super() calls the parent class's constructor. super.methodName() calls a parent class method. Forgetting super() in a subclass constructor causes a ReferenceError: Must call super constructor error.

Custom error classes

Custom errors that extend Error are one of the most useful class patterns in real code. They let you add custom properties (like service and recipient in the example) and use instanceof to catch specific error types:

try {
  await emailService.send(null, 'test');
} catch (error) {
  if (error instanceof NotificationError) {
    console.log('Notification failed:', error.service, error.recipient);
  } else {
    throw error; // Re-throw unexpected errors
  }
}

Static methods

Static methods belong to the class itself, not to instances. You call them directly on the class:

class User {
  static create(name, email) {
    // Factory method — creates and validates before returning
    if (!email.includes('@')) throw new Error('Invalid email');
    return new User(name, email);
  }
}

// Called on the class, not an instance
const user = User.create('Chuck', 'chuck@example.com');

AI uses static methods for factory patterns, utility functions attached to a class, and class-level configuration.

Classes in TypeScript

TypeScript adds type annotations and access modifiers to classes. AI generates TypeScript class syntax frequently:

class EmailNotification {
  private apiKey: string;       // Only accessible inside this class
  protected from: string;       // Accessible in this class and subclasses
  public serviceName: string;   // Accessible anywhere (default)

  constructor(apiKey: string) {
    this.apiKey = apiKey;
    this.from = 'noreply@myapp.com';
    this.serviceName = 'Email';
  }

  async send(recipient: string, message: string): Promise<{ success: boolean }> {
    // TypeScript knows the return type
    return { success: true };
  }
}

TypeScript also has a shorthand constructor syntax that declares and assigns in one step:

// Shorthand — TypeScript only
class EmailNotification {
  constructor(
    private apiKey: string,
    public serviceName: string = 'Email'
  ) {}
  // apiKey and serviceName are automatically assigned to this
}

What AI Gets Wrong About Classes

Forgetting super() in subclasses

AI sometimes generates a subclass constructor that uses this before calling super(). This always causes a runtime error. super() must be the first statement in a subclass constructor that uses this.

Using classes when functions would be cleaner

AI often reaches for a class when a simple function or object literal would be clearer and simpler. Modern JavaScript favors composition over inheritance. If AI generates a complex class hierarchy for a simple problem, ask it to refactor to functions.

This binding in callbacks

When a class method is passed as an event handler or callback, this loses its class binding. AI does not always handle this:

// ❌ Common AI mistake — this is undefined inside the callback
button.addEventListener('click', this.handleClick);

// ✅ Use arrow function class field (TypeScript / modern JS)
handleClick = () => {
  console.log(this.data); // 'this' is correctly bound
}

// ✅ Or bind in constructor
constructor() {
  this.handleClick = this.handleClick.bind(this);
}

Not using private fields for sensitive data

AI stores API keys and tokens as plain properties (this.apiKey) that are accessible from outside the class. Use TypeScript's private or JavaScript's native private fields (#apiKey) for sensitive data.

Classes vs. Functions Rule

In React, use function components and hooks — not classes. In Node.js services, libraries, and backend logic, classes are common and appropriate. If AI generates a class component for React, ask it to convert to a function component instead.

How to Debug Classes With AI

Common errors and causes

  • ReferenceError: Must call super constructor: Forgot super() in a subclass constructor before using this.
  • TypeError: Cannot read property of undefined (this): A method was called with lost this binding — use arrow functions or explicit bind.
  • TypeError: X is not a constructor: You are calling new on something that is not a class or constructor function — check the import/export.
  • TypeError: X.method is not a function: Either the method does not exist on the class, or you are calling it on null/undefined instead of an instance.

The debugging prompt

Debug Prompt

I'm getting [error message] when using this class:
[paste class definition]
Here's how I'm using it: [paste instantiation and method calls]
What's wrong and how do I fix it?

What to Learn Next

Next Step

Find a class in your AI-generated code and trace through it manually: identify the constructor, list what this properties it sets, and find every method. That 2-minute exercise builds the mental model faster than reading about it.

FAQ

A class is a blueprint for creating objects. It defines what data an object holds (properties set in the constructor) and what it can do (methods). You create specific objects from the blueprint using new ClassName().

A constructor is a special method inside a class that runs automatically when you create a new instance with new. It receives the arguments you pass and sets up the initial state of the object by assigning values to this properties.

this refers to the specific instance of the class that is being operated on. When you call myObject.doSomething(), this inside that method refers to myObject. Different instances each have their own this with their own data.

A class is the template or blueprint. An object (instance) is a specific thing created from that template using new. One class can create many objects, each with its own data but sharing the same methods defined in the class.

Not always. Modern React uses functional components and hooks rather than classes. However, classes appear frequently in Node.js backends, error handling, data modeling libraries, and frameworks like NestJS. Understanding them is necessary for reading AI-generated code, even if you prefer functional patterns.