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
orlogging
must be repeated or inlined - Inconsistent usage will creep in β one dev writes
.fetchSomething()
, another doesgetUserFromDb()
π§ 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.