A React renderer for HTTP servers. Define your routes declaratively with JSX.
Quick Start • API Reference • Advanced Usage • Example App • Contributing
Disclaimer: This is a fun, experimental project built for learning and exploration. It is not intended for production use. If you need a battle-tested HTTP framework, check out Express, Fastify, or Hono.
react-http-renderer uses a custom React Reconciler to transform JSX component trees into HTTP routing structures. Instead of rendering to the DOM, it renders to a fully functional HTTP server powered by Node's built-in http module.
const App = () => (
<Server port={3000}>
<Route path="/api/todos">
<Get handler={() => ({ todos: [] })} />
<Post handler={(ctx) => ({ created: ctx.body })} />
</Route>
</Server>
);
createServer(<App />).listen();- Declarative — Define routes as a component tree, not imperative method chains
- Composable — Nest routes, scope middleware, and extract patterns into reusable components
- Type-safe — Full TypeScript support with typed request context, handlers, and middleware
- Zero external dependencies — Only React and
react-reconciler— no Express, Koa, or Fastify - Familiar — If you know React, you already know the mental model
- Node.js ≥ 18
- pnpm ≥ 10 (recommended) or npm
npm install react-http-renderer reactOr from source:
git clone https://github.com/ntatoud/react-http.git
cd react-http
pnpm install
pnpm buildimport React from "react";
import { createServer, Server, Get } from "react-http-renderer";
const App = () => (
<Server port={3000}>
<Get handler={() => ({ message: "Hello, World!" })} />
</Server>
);
createServer(<App />).listen();
// => Server listening on http://localhost:3000| Component | Props | Description |
|---|---|---|
<Server> |
port |
Root server container. Wraps all routes and middleware. |
<Route> |
path |
Route group with a path prefix. Supports nesting. |
<Get> |
handler, path? |
Handles GET requests. |
<Post> |
handler, path? |
Handles POST requests. |
<Put> |
handler, path? |
Handles PUT requests. |
<Delete> |
handler, path? |
Handles DELETE requests. |
<Patch> |
handler, path? |
Handles PATCH requests. |
<Options> |
handler, path? |
Handles OPTIONS requests. |
<Head> |
handler, path? |
Handles HEAD requests. |
<Middleware> |
use |
Attaches a middleware function to the current scope. |
| Hook | Returns | Description |
|---|---|---|
useRequest() |
RequestContext |
Access the current request context inside components. |
useResponse() |
ServerResponse |
Access the raw Node.js response object. |
Every handler receives a RequestContext object:
interface RequestContext {
req: IncomingMessage; // Raw Node.js request
res: ServerResponse; // Raw Node.js response
params: Record<string, string>; // URL params (e.g. :id)
query: Record<string, string>; // Parsed query string
path: string; // Request path
method: HttpMethod; // HTTP method
body?: any; // Parsed JSON body
}| Return type | Behavior |
|---|---|
object / array |
Serialized as JSON with Content-Type: application/json |
string |
Sent as plain text |
undefined |
No automatic response — useful for streaming or manual res.end() |
Middleware functions receive the request context and a next function. Call next() to continue to the next middleware or handler. Skip next() to short-circuit the chain.
import { type MiddlewareHandler } from "react-http-renderer";
// Logging
const logger: MiddlewareHandler = async (ctx, next) => {
const start = Date.now();
await next();
console.log(`${ctx.method} ${ctx.path} ${ctx.res.statusCode} ${Date.now() - start}ms`);
};
// CORS
const cors: MiddlewareHandler = async (ctx, next) => {
ctx.res.setHeader("Access-Control-Allow-Origin", "*");
ctx.res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
if (ctx.method === "OPTIONS") {
ctx.res.statusCode = 204;
ctx.res.end();
return; // short-circuit
}
await next();
};
// Authentication guard
const auth: MiddlewareHandler = async (ctx, next) => {
const token = ctx.req.headers.authorization?.slice(7);
if (!token) {
ctx.res.statusCode = 401;
ctx.res.end(JSON.stringify({ error: "Unauthorized" }));
return;
}
await next();
};Middleware is scoped to the <Route> it's declared in. This lets you apply different middleware to different parts of your API without global state.
const App = () => (
<Server port={3000}>
{/* Global middleware */}
<Middleware use={logger} />
<Middleware use={cors} />
<Route path="/api">
{/* Public endpoints */}
<Route path="/users">
<Get handler={listUsers} />
<Post handler={createUser} />
<Route path="/:id">
<Get handler={getUser} />
<Put handler={updateUser} />
<Delete handler={deleteUser} />
{/* Deeply nested: /api/users/:id/posts */}
<Route path="/posts">
<Get handler={getUserPosts} />
</Route>
</Route>
</Route>
</Route>
{/* Protected endpoints */}
<Route path="/admin">
<Middleware use={auth} />
<Get path="/stats" handler={getStats} />
</Route>
</Server>
);// URL params: GET /users/42 => ctx.params.id === "42"
<Route path="/users/:id">
<Get handler={(ctx) => db.getUser(ctx.params.id)} />
</Route>
// Query strings: GET /search?q=react&limit=10
<Get path="/search" handler={(ctx) => {
const { q, limit = "10" } = ctx.query;
return db.search(q, parseInt(limit));
}} />Control HTTP status codes by setting ctx.res.statusCode before returning:
const getUser = (ctx: RequestContext) => {
const user = db.users.find((u) => u.id === ctx.params.id);
if (!user) {
ctx.res.statusCode = 404;
return { error: "User not found" };
}
return user;
};
const createUser = (ctx: RequestContext) => {
const { name, email } = ctx.body || {};
if (!name || !email) {
ctx.res.statusCode = 400;
return { error: "Validation failed", fields: ["name", "email"] };
}
ctx.res.statusCode = 201;
return db.createUser({ name, email });
};Return undefined to take full control of the response lifecycle:
const streamEvents = (ctx: RequestContext) => {
ctx.res.setHeader("Content-Type", "text/event-stream");
ctx.res.setHeader("Cache-Control", "no-cache");
let count = 0;
const interval = setInterval(() => {
ctx.res.write(`data: ${JSON.stringify({ count: ++count })}\n\n`);
if (count >= 5) {
clearInterval(interval);
ctx.res.end();
}
}, 1000);
return undefined;
};The repo includes a full-stack Todo application that demonstrates real-world usage of react-http-renderer.
apps/todo-app/
├── backend/ # REST API built with react-http-renderer
└── frontend/ # React + Vite client
# Start both frontend and backend in development mode
pnpm dev
# Or run them individually
pnpm --filter @todo-app/backend dev # API on http://localhost:3001
pnpm --filter @todo-app/frontend dev # Client on http://localhost:5173The backend defines a full CRUD API in a single JSX tree:
const App = () => (
<Server port={3001}>
<Middleware use={logger} />
<Middleware use={cors} />
<Route path="/api">
<Get path="/health" handler={() => ({ status: "ok" })} />
<Get path="/stats" handler={getStats} />
<Route path="/todos">
<Get handler={listTodos} />
<Post handler={createTodo} />
<Route path="/:id">
<Get handler={getTodo} />
<Put handler={updateTodo} />
<Delete handler={deleteTodo} />
</Route>
</Route>
</Route>
</Server>
);Endpoints:
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check |
GET |
/api/stats |
Todo statistics |
GET |
/api/todos |
List todos (supports ?completed=true|false) |
POST |
/api/todos |
Create a todo |
GET |
/api/todos/:id |
Get a todo by ID |
PUT |
/api/todos/:id |
Update a todo |
DELETE |
/api/todos/:id |
Delete a todo |
react-http/
├── packages/
│ └── react-http-renderer/ # Core renderer library
│ └── src/
│ ├── index.ts # Public API exports
│ ├── components.tsx # JSX components (Server, Route, Get, etc.)
│ ├── reconciler.ts # React Reconciler host config
│ ├── router.ts # Path matching, middleware, request handling
│ ├── server.ts # Server creation and lifecycle
│ ├── context.ts # React context (useRequest, useResponse)
│ └── types.ts # TypeScript type definitions
├── apps/
│ └── todo-app/
│ ├── backend/ # Example API server
│ └── frontend/ # Example React client
├── turbo.json # Turborepo pipeline config
└── pnpm-workspace.yaml # Workspace definitions
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Run tests across the monorepo
pnpm test
# Start dev mode (watches for changes)
pnpm dev
# Clean all build artifacts
pnpm cleanThe project has comprehensive test coverage across all packages:
# Run all tests
pnpm test
# Run tests for the core library only
pnpm --filter react-http-renderer test
# Run tests for the example app
pnpm --filter @todo-app/backend test
pnpm --filter @todo-app/frontend testTests include:
- Unit tests — Router path matching, query parsing, body parsing
- Integration tests — Full request/response cycles with middleware chains
- API tests — End-to-end CRUD operations on the todo example
| Layer | Technology |
|---|---|
| Renderer | React Reconciler |
| Runtime | Node.js built-in http module |
| Language | TypeScript 5.3 |
| Monorepo | Turborepo + pnpm workspaces |
| Testing | Vitest |
| Frontend | React + Vite |
| CI | GitHub Actions |
Contributions are welcome! Here's how to get started:
- Fork the repository
- Create a feature branch:
git checkout -b feat/my-feature - Commit your changes:
git commit -m "feat: add my feature" - Push to your branch:
git push origin feat/my-feature - Open a Pull Request
Please make sure all tests pass before submitting:
pnpm build && pnpm testThis project uses Changesets for version management and npm publishing.
If your PR includes changes to the react-http-renderer package, add a changeset before submitting:
pnpm changesetFollow the prompts to:
- Select the
react-http-rendererpackage - Choose the semver bump type (
patch/minor/major) - Write a summary of your changes
Commit the generated .changeset/*.md file with your PR.
PR with changeset ──> Merge to main ──> "Version Packages" PR created automatically
│
v
Merge Version PR ──> Published to npm
- PRs that include
.changeset/*.mdfiles are merged intomain - The release workflow automatically creates a "Version Packages" PR that:
- Consumes all pending changesets
- Bumps the version in
package.json - Updates
CHANGELOG.md
- When the "Version Packages" PR is merged, the workflow publishes the new version to npm and creates a GitHub release
For maintainers who need to publish manually:
pnpm build # Build all packages
pnpm version-packages # Consume changesets and bump versions
pnpm release # Build and publish to npmThis project uses npm trusted publishing via GitHub Actions OIDC — no static tokens required.
To enable automated publishing after the initial release:
- Go to your package on npmjs.com → Settings → Publishing access
- Under Trusted publishing, add a publisher:
- Repository owner:
ntatoud - Repository name:
react-http - Workflow filename:
release.yml
- Repository owner:
- Published packages will include verified provenance statements
This project is licensed under the MIT License.
Built with React and a custom Reconciler.
If you find this useful, consider giving it a star!
