JSPM

  • Created
  • Published
  • Downloads 13
  • Score
    100M100P100Q52580F
  • License ISC

Remix/React Router 7 Mediator

Package Exports

  • @remediator/core

Readme

@remediator/core

⚑ CQRS-style mediator pattern for frontend apps (Remix, React Router 7, Vite/Webpack support)

Minimal, convention-based mediator designed for frontend frameworks like Remix and React Router 7. Automatically registers your *.mediator.ts files for clean, scalable architecture.


πŸ”§ Install

npm install @remediator/core

πŸš€ Quick Start

1. Create a Query or Command

// /app/user/GetUserQuery.mediator.ts
export class GetUserQuery {}

export class GetUserQueryHandler {
  async handle(query: GetUserQuery) {
    // fetch user data here
    return { id: "u1", name: "Jeremy" };
  }
}

βœ… File name must end in .mediator.ts
βœ… Class names must follow this convention:

  • XQuery + XQueryHandler
  • XCommand + XCommandHandler

Every Query or Command must have a corresponding Handler class in the same file.


2. Auto-Register Your Files

In Remix (e.g. server.entry.ts or root loader):

import { registerAll } from "@remediator/core";

registerAll();

In React Router 7:

import { registerAll } from "@remediator/core";

export function loader() {
  registerAll(); // Safe to call multiple times (runs once)
}

3. Send a Command or Query

import { reMediator } from "@remediator/core";

const result = await reMediator.send(new GetUserQuery(), request);

πŸ’‘ Using Middleware

Middleware lets you inject behavior (auth, logging, etc.) into the send pipeline.

Example: authMiddleware

import type { Pipeline } from "@remediator/core";
import { getSession } from "./session.server";
import { UnauthorizedError } from "~/shared/lib/errors.server";

const publicRoutes = ["/login", "/register", "/forgot-password"];

export const authMiddleware: Pipeline = async (req, context, next) => {
  const cookie = context.rawRequest.headers.get("Cookie");
  const session = await getSession(cookie);
  const userId = session.get("userId");

  if (publicRoutes.includes(context.rawRequest.url)) {
    return next();
  }

  if (!userId) {
    throw new UnauthorizedError();
  }

  context.userId = userId;
  return next();
};

Register it:

import { reMediator } from "@remediator/core";
import { authMiddleware } from "./auth.middleware";

reMediator.use(authMiddleware);

πŸ§ͺ Usage Examples in Remix

βœ… Loader Example

import { reMediator } from "@remediator/core";
import { redirect, type LoaderFunctionArgs } from "@remix-run/node";
import { UnauthorizedError } from "~/shared/lib/errors.server";

export async function loader({ request }: LoaderFunctionArgs) {
  try {
    const user = await reMediator.send(new GetUserQuery(), request);
    const cycles = await reMediator.send(new GetCyclesForUserQuery(), request);
    const projects = await reMediator.send(
      new GetProjectsForUserQuery(),
      request
    );

    return data({ user, cycles, projects });
  } catch (error) {
    if (error instanceof UnauthorizedError) {
      return redirect("/");
    }
    throw error;
  }
}

βœ… Action Example

import { reMediator } from "@remediator/core";
import { redirect, type ActionFunctionArgs } from "@remix-run/node";

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const dataEntries = Object.fromEntries(formData);

  if (dataEntries.delete === "true") {
    const result = await reMediator.send<IResponse>(
      new ProjectDeleteCommand(
        dataEntries.id as string,
        dataEntries.deleteCycles === "true"
      ),
      request
    );
    if (result.success) {
      return redirect(`/app/projects`);
    }
    return data({ errors: result.errors }, { status: 400 });
  }

  const result = await reMediator.send<IResponse>(
    new ProjectUpsertCommand(
      dataEntries.id as string,
      dataEntries.name as string
    ),
    request
  );

  if (result.success) {
    return redirect(`/app/project/${result.id}`);
  }

  return data({ errors: result.errors }, { status: 400 });
}

🧠 send() Arguments

reMediator.send(request: IRequest, rawRequest: Request, middleware?: Array<Pipeline | string>)

Middleware Options:

  • undefined: use all registered middleware (default)
  • []: use no middleware
  • ["authMiddleware"]: use only named middleware
  • [authMiddleware]: pass in specific middleware functions

βš™οΈ Configuring registerAll(options)

registerAll({
  path: "/app", // folder to scan
  includeFileNames: ["*.mediator.ts"],
});

βœ… Summary of Conventions

Type File Format Class Format
Command *.mediator.ts XCommand + XCommandHandler
Query *.mediator.ts XQuery + XQueryHandler
Middleware *.mediator.ts Ends with Middleware function name

All handlers must be in the same file as their command/query. Auto-registration depends on this structure.


πŸ“¦ Project Structure Example

/app
  /auth
    AuthMiddleware.mediator.ts
  /GetUser
    GetUserQuery.mediator.ts
    GetUserQueryHandler.mediator.ts
  /UpsertProject
    UpsertProjectCommand.mediator.ts
    UpsertProjectCommandHandler.mediator.ts

🎯 The Purpose of Mediator (@remediator/core)

It creates a backend for frontend-compatible command/query dispatching system that:

  • βœ… Promotes thin loader/action functions
  • βœ… Enables decoupled architecture (handlers are registered separately from where they’re called)
  • βœ… Encourages single-responsibility by placing logic in handler files instead of route modules
  • βœ… Supports middleware pipelines for cross-cutting concerns (auth, logging, etc.)
  • βœ… Makes testing and mocking simpler and more isolated
  • βœ… Gives a foundation for a scalable pattern β€” especially in large projects or multi-team repos

πŸ€” Could You Just Call a Class Directly?

Sure. Here’s the direct version:

// inside loader.ts
const result = await myStaticServiceClass.fetchSomething();

But this becomes a problem over time:

  • It couples route files with implementation
  • It breaks testability β€” you now must test .fetchSomething() behavior within route logic
  • Middleware like auth or logging must be repeated or inlined
  • Inconsistent usage will creep in β€” one dev writes .fetchSomething(), another does getUserFromDb()

🧠 Why Mediator Is Better for Long-Term Projects

This:

const user = await reMediator.send(new GetUserQuery(), request);

Becomes a universal pattern.

Every command/query goes through:

  • Centralized handler logic
  • Standard middleware flow
  • Uniform lifecycle (context, validation, etc.)

It aligns with CQRS/clean architecture principles:

  • 🧱 Routes = controllers (they delegate)
  • 🧱 Handlers = use-cases
  • 🧱 Commands/Queries = contracts
  • 🧱 Middleware = cross-cutting services

βš–οΈ Tradeoff Summary

Approach Pros Cons
Direct class method Simple, fast for small projects Spaghetti later, breaks SRP, no middleware
Mediator Clean, testable, scalable, maintainable Slight boilerplate, needs naming convention

βœ… If you stick with this pattern, you:

  • Make onboarding easier
  • Reduce duplication
  • Centralize flow and logic
  • Open up testing, logging, validation, and auth at the middleware level

✨ Credits

Created by @remediator β€” solving CQRS needs for modern frontend frameworks.