mdashikjs/blog
All posts
Function Composition Is the Refactor I Reach for First
Frontend

Function Composition Is the Refactor I Reach for First

Frontend4 min

Function Composition Is the Refactor I Reach for First

Inheritance felt natural in Java class. In JavaScript it is almost always the wrong tool. Small functions plus `pipe` solve the same problems with less ceremony and more flexibility.

JavaScriptFunctional ProgrammingArchitecture
Share:

pipe in 5 lines

const pipe = (...fns) => (input) => fns.reduce((acc, fn) => fn(acc), input);

That is it. pipe(f, g, h)(x) becomes h(g(f(x))) but reads left-to-right, which matches how you think about the transformation.

Where I use it

Any data transformation with multiple stages:

const normalizeUser = pipe(
  trimStrings,
  lowercaseEmail,
  removeNullFields,
  addTimestamps,
);

const clean = normalizeUser(rawInput);

Each step is a tiny, pure, independently testable function. The pipeline itself is data — you can build, extend, or reorder it at runtime.

The class alternative

class UserNormalizer {
  normalize(input) {
    return this.addTimestamps(
      this.removeNullFields(
        this.lowercaseEmail(
          this.trimStrings(input),
        ),
      ),
    );
  }
  // four methods, all `this`-bound, nested pyramid
}

More code, reversed reading order, and now I have a class I will need to instantiate and possibly subclass. For a transform pipeline, nothing about a class earns its weight.

Async composition

pipe is synchronous by default. For async steps, use an async variant:

const pipeAsync = (...fns) => (input) =>
  fns.reduce((p, fn) => p.then(fn), Promise.resolve(input));

const loadAndNormalizeUser = pipeAsync(
  fetchUser,
  fetchProfile,
  normalizeUser,
);

const user = await loadAndNormalizeUser(userId);

Each step can be sync or async. await flows through naturally.

Composition in React

React embraces composition over inheritance explicitly. Higher-order components are pipe for components. Hooks are composable pieces of logic — useUser, usePermissions, useAuditLog snap together without inheritance. If you have ever felt the pull to subclass a React component, the answer is almost always "extract a hook."

Middleware as composition

Express, Koa, NestJS interceptors — middleware is composition with an escape hatch:

app.use(logger);
app.use(authenticate);
app.use(rateLimit);
app.use(handler);

Each middleware wraps the next. The request flows through in order. Swap an item, reorder, remove — no class hierarchy to untangle.

The limits of composition

For stateful, interactive objects — a WebSocket wrapper, a parser with accumulated buffer — a class or closure with methods is often clearer than a pipeline. Composition excels at transformations. It strains when the subject has identity and lifetime.

The refactor I keep doing

When I see a class with mostly static methods, or a class whose only state is passed through constructor arguments — I replace it with functions and a pipeline. The code shrinks, tests get simpler, and the "what does this do" reading experience improves. Inheritance has its place. It is just smaller than I used to think.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.