JSPM

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

Remix/React Router 7 Mediator

Package Exports

  • @remediator/core

Readme

@remediator/core

Lightweight frontend mediator for Remix, React Router, Vite & Webpack.

A simple CQRS-style mediator that lets you register handlers and middleware, and dispatch commands or queries with minimal ceremony.


🔧 Install

npm install @remediator/core

📁 Conventions

  • Request classes implement IRequest<TResult>
  • Handlers implement IRequestHandler<TRequest, TResult>
  • Registration pairs a request ctor with its handler via reMediator.register(...)
  • Middleware are functions matching (req, ctx, next) => Promise<TResult> and added with reMediator.use(...)

🚀 Setup

Register your handlers and middleware once at app startup:

// register handlers
reMediator.register(GetUserQuery, new GetUserQueryHandler());
reMediator.register(UpdateOrderCommand, new UpdateOrderCommandHandler());

// register global middleware (optional)
reMediator.use(authMiddleware);

Auto-Registration (Optional - Development)

Use the Vite plugin for automatic registration during development:

1. Configure the plugin

// vite.config.ts
import { reMediatorPlugin } from "@remediator/core";

export default defineConfig({
  plugins: [
    reMediatorPlugin({
      path: "app", // where to look for mediator files
      includeFileNames: ["*.mediator.ts"], // file patterns to include
      outputDir: "remediator", // where to generate client (default: 'remediator')
    }),
  ],
});

2. Create mediator files

// app/modules/users/getUser/getUserQuery.mediator.ts
export class GetUserQuery implements IRequest<User> {
  constructor(public id: string) {}
}

export class GetUserQueryHandler
  implements IRequestHandler<GetUserQuery, User>
{
  async handle(query: GetUserQuery, context?: IRequestContext): Promise<User> {
    // Your handler logic
    return await getUserById(query.id);
  }
}

3. Import the generated client

// In your loader/action
import { reMediator } from "~/remediator/client";

export async function loader({ request }: LoaderFunctionArgs) {
  const user = await reMediator.send(GetUserQuery, { id: "u1" }, request);
  return json({ user });
}

The plugin generates:

  • remediator/client.ts - Auto-registered reMediator instance
  • remediator/manifest.json - List of discovered mediator files

📡 Dispatching Requests

reMediator.send() has two overloads:

  1. Constructor + data: instantiate + hydrate

    await reMediator.send(
      GetUserQuery, // request class
      { id: "u1" }, // partial properties
      request, // raw Request object
      [logMiddleware] // optional per-call middleware
    );
  2. Instance: use an existing request object

    const cmd = new UpdateOrderCommand();
    cmd.orderId = "A123";
    cmd.status = "Shipped";
    
    await reMediator.send(
      cmd, // fully-built instance
      request,
      [auditMiddleware] // optional
    );

Both return the handler's result type.


🛠️ Middleware

Middleware run before the handler and can:

  • Examine or modify req fields
  • Read/request context via ctx.rawRequest
  • Short-circuit or throw errors
  • Invoke await next() to continue the pipeline
export const authMiddleware: Pipeline = async (req, ctx, next) => {
  const token = ctx.rawRequest.headers.get("Authorization");
  if (!token) throw new UnauthorizedError();
  ctx.user = verifyToken(token);
  return next();
};

📦 Full Example

Manual Registration

// Handler registration
reMediator.register(GetUserQuery, new GetUserQueryHandler());
reMediator.use(authMiddleware);

// In a Remix loader
export async function loader({ request }: LoaderFunctionArgs) {
  // 1) Ctor + data
  const user1 = await reMediator.send(GetUserQuery, { id: "u1" }, request);

  // 2) Instance
  const q = new GetUserQuery();
  q.id = "u2";
  const user2 = await reMediator.send(q, request);

  return json({ user1, user2 });
}

Auto-Registration

// File: app/modules/users/getUser/getUserQuery.mediator.ts
export class GetUserQuery implements IRequest<User> {
  constructor(public id: string) {}
}

export class GetUserQueryHandler
  implements IRequestHandler<GetUserQuery, User>
{
  async handle(query: GetUserQuery): Promise<User> {
    return await getUserById(query.id);
  }
}

// File: app/routes/users.$id.tsx
import { reMediator } from "~/remediator/client";

export async function loader({ request, params }: LoaderFunctionArgs) {
  const user = await reMediator.send(GetUserQuery, { id: params.id }, request);
  return json({ user });
}

🔧 Plugin Options

reMediatorPlugin({
  path: "app", // Directory to search (default: 'app')
  includeFileNames: ["*.mediator.ts"], // File patterns to include
  outputDir: "remediator", // Output directory (default: 'remediator')
});

Simple, explicit, and easy to test — perfect for clean frontend CQRS.

© 2025 @remediator/core