JSPM

  • Created
  • Published
  • Downloads 13
  • Score
    100M100P100Q52510F
  • 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
    GetUserQuery.mediator.ts
    GetCyclesForUserQuery.mediator.ts
    AuthMiddleware.mediator.ts
    ProjectUpsertCommand.mediator.ts

โœจ Credits

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