React Router
Mount a file-based Taser.js REST API under /api in a React Router framework-mode app, with a typed client for loaders and components.
React Router in framework mode owns the server, so Taser.js is mounted as a resource route that forwards every /api request to the compiled Taser.js app. The Vite plugin keeps the route manifest in sync while react-router dev runs.
React Router deploys through its own server build and adapters, so Taser.js adds no deployment configuration.
Scaffold
npm create taserjs@latest my-app -- --framework react-routercreate-taserjs runs create-react-router, adds Taser.js, and skips the deployment question.
Manual Setup
1. Install
pnpm add @taserjs/router @taserjs/client
pnpm add -D @taserjs/plugin @taserjs/cli2. Register the Vite plugin
Add taser() before reactRouter() in vite.config.ts:
import { reactRouter } from "@react-router/dev/vite";
import { taser } from "@taserjs/plugin/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [taser(), reactRouter()],
});The plugin detects React Router and leaves request handling to it.
3. Create the app definition
Point Taser.js at a folder inside app/:
import { defineConfig } from "@taserjs/cli";
export default defineConfig({
serverDir: "app/server",
routesDir: "routes",
outputDir: ".taserjs",
app: "taser.ts",
});Create app/server/taser.ts with defineTaser().basePath("/api") and your endpoints in app/server/routes/, exactly as in the Next.js guide.
4. Mount the API
A route module without a default export is a resource route. React Router calls loader for GET and HEAD and action for every other method, so one handler serves them all:
import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
import { app } from "../server/.taserjs/routes.gen";
const handle = ({ request }: LoaderFunctionArgs | ActionFunctionArgs) => app.fetch(request);
export const loader = handle;
export const action = handle;Register it with a splat route, which also matches the bare /api:
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("api/*", "routes/api.ts"),
] satisfies RouteConfig;5. Create the typed client
import { createClient } from "@taserjs/client";
import type { AppManifest } from "./server/.taserjs/routes.gen";
export const api = createClient<AppManifest>({ baseUrl: "/api" });A relative base URL works in the browser. Server-side loaders run fetch in Node, which cannot resolve it, so create a client with an absolute baseUrl there or call app.fetch directly.
Project Structure
my-react-router-app/
├── vite.config.ts # plugins: [taser(), ..., reactRouter()]
├── taserjs.config.ts # serverDir: "app/server"
└── app/
├── routes.ts # route("api/*", "routes/api.ts")
├── api.ts # createClient<AppManifest>({ baseUrl: "/api" })
├── routes/
│ ├── home.tsx
│ └── api.ts # forwards /api/** to Taser.js
└── server/ # Taser.js REST API subsystem
├── taser.ts
├── context.ts
├── routes/
│ └── index.get.ts # GET /api
└── .taserjs/
└── routes.gen.ts # generatedUsing the Client
Call the API from a client-side effect or a clientLoader, where the relative base URL resolves:
import { useEffect, useState } from "react";
import { api } from "../api";
export default function Home() {
const [message, setMessage] = useState<string>();
useEffect(() => {
api
.$get()
.then((res) => res.json())
.then((data) => setMessage(data.message));
}, []);
return <p>{message}</p>;
}