I’m struggling to properly structure a JavaScript class for my project. My methods are getting messy, I’m not sure what should be static vs instance, and I’m confused about how to organize properties and inheritance. Can someone explain best practices with simple examples so I can refactor my current code and avoid bad patterns?
Think of a class as “what an object is” plus “what it does”.
- What should be instance vs static
Use instance properties and methods when they depend on data of one object.
Example:
class User {
constructor(name, email) {
this.name = name
this.email = email
}
sendEmail(message) {
console.log(To ${this.email}: ${message})
}
}
Here name, email, sendEmail are about one user.
Use static when the method does not care about a specific instance, or creates one.
class User {
constructor(name, email) {
this.name = name
this.email = email
}
static fromJson(json) {
const data = JSON.parse(json)
return new User(data.name, data.email)
}
static isValidEmail(email) {
return email.includes(‘@’)
}
}
Rule of thumb
If you use this inside the method, make it instance.
If you never use this, consider static or move it outside the class.
- Organizing properties
Keep constructor small. Only assign inputs and default values. No big logic.
Bad:
constructor(config) {
// parse config
// fetch remote data
// start timers
// etc
}
Better:
constructor(config) {
this.config = this.normalizeConfig(config)
this.state = { ready: false }
}
async init() {
await this.loadRemoteData()
this.state.ready = true
}
You end up with:
constructor: set fields
init/setup: heavy work
public methods: what others use
private helpers: internal only
For private helpers in modern JS:
class UserService {
#cache = new Map()
#normalizeUser(user) {
// internal helper
}
getUser(id) {
// uses #cache and #normalizeUser
}
}
- Inheritance vs composition
Use inheritance when you have a clear “is a” relation and you want to share behavior.
Example:
class Shape {
area() {
throw new Error(‘Not implemented’)
}
}
class Circle extends Shape {
constructor(radius) {
super()
this.radius = radius
}
area() {
return Math.PI * this.radius * this.radius
}
}
If you keep fighting the parent class API, stop and use composition.
Composition example:
class Logger {
log(msg) {
console.log(msg)
}
}
class UserService {
constructor(logger) {
this.logger = logger
}
addUser(user) {
this.logger.log(‘Adding user’)
}
}
UserService “has a” Logger. It does not extend it.
- Keeping methods from getting messy
Group methods mentally:
Public API
Things other modules call. Keep this list small and clear.
Internal helpers
Name them clearly. Example: validateInput, buildRequest, parseResponse.
Try:
class Thing {
constructor() { … }
// Public
doAction() { … }
stop() { … }
// Internal
#validateOptions(opts) { … }
#runStepOne() { … }
#runStepTwo() { … }
}
When a method grows past 20 to 30 lines, split it into smaller helpers.
- How to decide where stuff goes
Quick checklist when you add something:
Does it use this or instance fields
Yes → instance method
No and pure function → static or separate utility module
Does this class start to do more than one clear responsibility
Yes → split into two classes or a helper module
Do child classes override many parent methods in weird ways
Yes → rethink inheritance, use composition
- Simple full example
class Cart {
constructor() {
this.items =
}
addItem(productId, qty) {
const item = this.#findItem(productId)
if (item) {
item.qty += qty
} else {
this.items.push({ productId, qty })
}
}
removeItem(productId) {
this.items = this.items.filter(i => i.productId !== productId)
}
getTotal(priceProvider) {
return this.items.reduce((sum, item) => {
const price = priceProvider.getPrice(item.productId)
return sum + price * item.qty
}, 0)
}
#findItem(productId) {
return this.items.find(i => i.productId === productId)
}
static fromJson(json) {
const data = JSON.parse(json)
const cart = new Cart()
cart.items = data.items ||
return cart
}
}
Here:
Instance: items, addItem, removeItem, getTotal
Private helper: #findItem
Static: fromJson
If you want, post a short version of your class and people can suggest a concrete refactor.
You’re not alone, JS classes get messy fast.
I mostly agree with @caminantenocturno, but I’d tweak a couple of their “rules” so they’re more practical in real projects.
1. Static vs instance: think about who calls it, not just this
Their rule of thumb “if it uses this, make it instance” is useful, but I’d flip the perspective:
Ask: who is supposed to call this method?
-
If it’s about an existing thing → instance method
cart.addItem(productId, qty) user.sendEmail(message) socket.close() -
If it’s about creating / looking up / validating things in general → usually static or even better: separate module
User.fromJson(json) // static is fine User.isValidEmail(email) // honestly, this could be a pure function in a util file
I actually prefer utility modules over static methods for anything that is:
- stateless
- reusable across multiple classes
- not conceptually “owned” by that class
Example:
// emailUtils.js
export function isValidEmail(email) {
return email.includes('@')
}
Then your class stays focused instead of turning into “that one god-class with all the static helpers”.
2. Properties & constructor: define the shape of the instance upfront
Constructor should mostly answer: “What does an instance always have?”
I try to make every property visible in the constructor, even with defaults, so I can glance and understand the object’s shape:
class Order {
constructor({ id, userId }) {
this.id = id
this.userId = userId
this.items = []
this.status = 'pending'
this.createdAt = new Date()
this.updatedAt = null
}
}
Avoid “hidden” properties that appear later in random methods:
// Harder to reason about:
applyDiscount(code) {
if (!this.discounts) this.discounts = [] // property appears out of nowhere
this.discounts.push(code)
}
Better:
constructor(...) {
// ...
this.discounts = []
}
That makes refactoring & debugging a lot less painful.
3. Inheritance: use it rarely, and when you do, make it boring
Honestly, JS class inheritance is overused. I’d almost say:
If you’re not 100% sure you need inheritance, you probably don’t.
Where it does work well:
- UI components with a very stable base API
- libraries where you’re designing explicit “extension points”
- simple polymorphism that doesn’t fight you
Example:
class Storage {
save(key, value) {
throw new Error('Not implemented')
}
}
class MemoryStorage extends Storage {
constructor() {
super()
this.map = new Map()
}
save(key, value) {
this.map.set(key, value)
}
}
When it starts feeling like:
- your child classes override half the parent’s behavior
- parent has flags like
this.type = 'circle'that children depend on supercalls are confusing
…that’s usually a sign you really wanted composition.
Composition pattern:
class OrderService {
constructor({ storage, logger }) {
this.storage = storage
this.logger = logger
}
createOrder(order) {
this.logger.info('Creating order')
this.storage.save(order.id, order)
}
}
Swapping behaviors is easier with composition than with inheritance.
4. Keeping methods from turning into a dumpster fire
When a method feels messy, it’s usually because it is doing multiple responsibilities:
Bad-ish:
async processOrder(orderData) {
// validate
// transform
// calculate totals
// store
// send emails
// log stuff
}
Refactor into:
async processOrder(orderData) {
const validData = this.#validateOrder(orderData)
const normalized = this.#normalizeOrder(validData)
const totals = this.#calculateTotals(normalized)
await this.repository.save(normalized, totals)
await this.notificationService.sendOrderEmail(normalized)
this.logger.info('Processed order', { id: normalized.id })
}
And then each private method is short and readable. I don’t care if a method has 15 lines or 40, what I care is: can I give it a single honest verb phrase name?
If not, split it.
5. One class, one story
If you’re unsure where to put a method or property, ask:
“If I had to explain this class to a junior dev in one sentence, what would I say?”
Example:
Cart→ “Represents a shopping cart and operations on it”CartService→ “Coordinates saving/loading carts and applying business rules”CartRepository→ “Reads/writes cart data from storage”
If you catch yourself saying:
“Well, it kind of manages orders, AND handles HTTP requests, AND also logs stuff…”
You’ve got too many roles in one class. Split.
6. Practical checklist you can keep by your editor
When you add a new thing:
-
Does it interact with one instance’s state?
- Yes → instance method / property
- No → either static or better: a plain function in a module
-
Is this class now responsible for more than 1 coherent idea?
- Yes → extract a new class or service
-
Does this really need to be a class at all?
Sometimes a factory function + closure is cleaner:function createCounter() { let value = 0 return { inc() { value++ }, get() { return value } } }
Classes are a tool, not a religion.
If you want more concrete help, post a simplified version of your actual class (or at least its public methods and main fields). Even a trimmed-down, slightly fake version is enough to suggest a clearer structure.
Think about your class from three angles that complement what @stellacadente and @caminantenocturno already covered:
- Lifecycle, not just structure
They focused a lot on what goes where, which is solid. I’d add: explicitly design the lifecycle of your object.
Example mental model:
constructor→ object exists but is not readyinit()orconnect()→ object is readydestroy()orclose()→ object is no longer usable
class ChatClient {
constructor(config) {
this.config = config
this.socket = null
this.isConnected = false
}
async connect() {
this.socket = await this.#openSocket(this.config.url)
this.isConnected = true
}
send(message) {
if (!this.isConnected) {
throw new Error('Client not connected')
}
this.socket.send(message)
}
close() {
if (this.socket) {
this.socket.close()
this.socket = null
this.isConnected = false
}
}
async #openSocket(url) {
// hidden complexity
}
}
When you think in lifecycle, it becomes clearer which methods belong to the object and when they should be callable.
- Boundaries: class vs module vs function
I partially disagree with the “static methods vs instance” focus. A lot of the stuff people cram into classes could be plain functions in a module.
Rule I use:
- Has identity and state over time → class
- Pure transformation or validation → standalone function
- Cross cutting helpers (e.g. formatting, parsing) → utility module
// priceUtils.js
export function applyTax(amount, rate) {
return amount + amount * rate
}
Your class then only calls applyTax, instead of exposing Order.applyTaxStatic() just because it feels “object oriented.”
- Role naming instead of “god classes”
If your class names are vague, your structure will follow. You do not want a Manager, Handler, Controller, or Service that does everything.
Take a messy situation like:
class UserManager {
// loads, validates, logs in, sends email, formats UI data...
}
Refactor first by naming intent:
UserRepository→ talks to storageUserAuthenticator→ login / tokensUserNotifier→ emails, notificationsUserPresenter→ transforms to view models
Once the names are clear, method placement is almost obvious. This is where I think both @stellacadente and @caminantenocturno stop one step early: they give great rules, but if the class name itself is fuzzy, those rules are hard to apply.
- Structural patterns inside a single class
To keep big classes understandable, I often follow an internal layout pattern:
class Something {
// 1. Static factory / metadata
static kind = 'something'
static createDefault() { ... }
// 2. Constructor & basic state
constructor(opts) { ... }
// 3. Public API in “story order”
start() { ... }
stop() { ... }
toJSON() { ... }
// 4. Event handlers (if any)
#onMessage(msg) { ... }
#onError(err) { ... }
// 5. Low-level helpers
#parseInput(input) { ... }
#buildPayload(data) { ... }
}
The idea is you can scroll the file and it reads like a narrative instead of a random grab bag of methods.
- Quick smell tests
When you are unsure what is wrong with your class, check for these:
- A method touches more than 3 unrelated instance fields → likely doing too much
- Changing one feature forces edits in many methods across the class → class has multiple responsibilities
- Unit testing requires elaborate setup or stubbing many dependencies → class is too coupled
In those cases, try extracting a sub-object:
class CartPricing {
constructor(taxRules, discountRules) {
this.taxRules = taxRules
this.discountRules = discountRules
}
calculate(cartItems) {
// all pricing logic here
}
}
class Cart {
constructor(pricing) {
this.items = []
this.pricing = pricing
}
getTotal() {
return this.pricing.calculate(this.items)
}
}
Now your main class stays smaller and easier to reason about.
- On “pros & cons” for classes in general
Since you mentioned struggling with classes, here is a quick pros / cons style view that also applies directly to how you structure things:
Pros
- Good for modeling entities with long lived state
- Encourages grouping data plus behavior
- Easier mental model for people coming from Java / C#
- Plays well with dependency injection patterns
Cons
- Easy to overuse for pure logic that should be functions
- Static methods can turn into dumping grounds
- Inheritance in JS gets messy quickly if misused
- Makes some unit tests harder than with pure functions
@stellacadente leans a bit more on conceptual clarity and inheritance examples, @caminantenocturno adds pragmatic tweaks and utility-module ideas. If you mix their guidelines with the lifecycle & naming focus above, your classes should start to feel a lot less chaotic.
If you want targeted help, post a trimmed version of one real class: constructor, field list, and public methods only. It is often possible to refactor it into something cleaner with just a few focused changes.