
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
A type-safe router for React applications that puts TypeScript first, ensuring your routes, parameters, and query strings are always in sync with your code.
A type-safe router for React applications that puts TypeScript first, ensuring your routes, parameters, and query strings are always in sync with your code.
When building React applications, keeping your routing configuration in sync with your components can be challenging. URLs change, parameters get renamed, and query strings evolve - but your TypeScript compiler doesn't know about any of it. Until now.
Brouther solves this by creating a single source of truth for your routes that TypeScript understands deeply. This means:
/user/:id are type-checked at compile timenpm install brouther
# or
yarn add brouther
# or
pnpm add brouther
Let's build a simple application to understand how Brouther works:
// router.ts
import { createRouter } from 'brouther';
import HomePage from './pages/HomePage';
import UserProfile from './pages/UserProfile';
import ProductList from './pages/ProductList';
// Define your routes with full type information
export const router = createRouter([
{
id: 'home',
path: '/',
element: <HomePage />
},
{
id: 'userProfile',
path: '/user/:userId',
element: <UserProfile />
},
{
id: 'products',
path: '/products?category=string&sort=string&page=number',
element: <ProductList />
}
] as const); // The 'as const' is crucial for type inference!
Now, let's use it in your application:
// App.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Brouther, Outlet } from 'brouther';
import { router } from './router';
function App() {
return (
<Brouther config={router.config}>
<div className="app">
<Navigation />
<main>
<Outlet />
</main>
</div>
</Brouther>
);
}
// Navigation.tsx
import { Link } from 'brouther';
import { router } from './router';
function Navigation() {
return (
<nav>
{/* Simple link - no parameters needed */}
<Link href={router.links.home}>Home</Link>
{/* Link with path parameter - TypeScript knows userId is required! */}
<Link
href={router.links.userProfile}
paths={{ userId: '123' }}
>
View Profile
</Link>
{/* Link with query parameters - all typed! */}
<Link
href={router.links.products}
query={{
category: 'electronics',
sort: 'price',
page: 1
}}
>
Electronics
</Link>
</nav>
);
}
Brouther uses a special syntax in route paths to define parameters and query strings:
// Dynamic path parameters use :paramName
"/user/:userId"; // userId will be a required string parameter
// Query strings are defined after ?
"/products?category=string"; // category is an optional string
// Required query parameters use !
"/products?category=string!"; // category is now required
// Array query parameters use []
"/products?tags=string[]"; // tags is an optional string array
// Combine multiple query parameters with &
"/products?category=string&tags=string[]&inStock=boolean";
// You can even type your parameters
"/post/:postId?published=date&author=string";
Here's where Brouther really shines. Let's look at how TypeScript helps you:
// ❌ This will cause a TypeScript error - missing required path
<Link href={router.links.userProfile}>Profile</Link>
// ❌ This will also error - wrong parameter name
<Link
href={router.links.userProfile}
paths={{ id: '123' }} // Should be userId!
>
// ❌ TypeScript catches type mismatches too
<Link
href={router.links.products}
query={{ page: '1' }} // Should be a number!
>
// ✅ This is correct - TypeScript is happy!
<Link
href={router.links.userProfile}
paths={{ userId: '123' }}
>
Brouther provides hooks to access route information with full type safety:
// UserProfile.tsx
import { usePaths, useQueryString } from 'brouther';
import { router } from './router';
function UserProfile() {
// Get typed path parameters
const paths = usePaths(router.links.userProfile);
// paths.userId is typed as string
// For the products page, you'd get typed query parameters
const query = useQueryString(router.links.products);
// query.category is string | undefined
// query.page is number | undefined
// query.sort is string | undefined
return <div>User ID: {paths.userId}</div>;
}
Brouther supports data loading and form actions, similar to modern routing libraries but with full type safety:
const router = createRouter([
{
id: 'userProfile',
path: '/user/:userId',
element: <UserProfile />,
// Loader runs before the component renders
loader: async ({ paths, queryString }) => {
// paths.userId is typed!
const user = await fetchUser(paths.userId);
return jsonResponse(user);
},
// Actions handle form submissions
actions: async () => ({
post: async ({ form, paths }) => {
const formData = formToJson(form);
await updateUser(paths.userId, formData);
return redirectResponse('/success');
}
})
}
]);
// In your component
function UserProfile() {
const data = useDataLoader<typeof loader>();
// data is fully typed based on your loader!
}
Brouther provides elegant error handling with error boundaries:
const router = createRouter([
{
id: 'userProfile',
path: '/user/:userId',
element: <UserProfile />,
errorElement: <UserErrorPage />, // Shown if loader fails
loadingElement: <UserSkeleton /> // Shown while loading
}
]);
// Global error handling
<Brouther
config={router.config}
ErrorElement={<NotFoundPage />} // For 404s
>
<App />
</Brouther>
Brouther includes a type-safe Form component that integrates with your routes:
import { Form } from 'brouther';
function EditProfile() {
const actions = useFormActions();
return (
<Form method="post">
<input name="name" />
<input name="email" type="email" />
<button type="submit">
{actions.loading ? 'Saving...' : 'Save'}
</button>
</Form>
);
}
Navigate programmatically with full type safety:
function SomeComponent() {
const navigation = useNavigation();
const handleClick = () => {
// Type-safe navigation
navigation.push(
router.link(
router.links.userProfile,
{ userId: '123' } // Required!
)
);
};
return <button onClick={handleClick}>Go to Profile</button>;
}
createRouter(routes, basename?, options?)Creates a router configuration with type-safe routes.
const router = createRouter(
[...routes],
"/app", // optional basename
{
sensitiveCase: false, // optional: case-sensitive matching
history: createBrowserHistory, // optional: custom history
}
);
createMappedRouter(routeMap, basename?, options?)Alternative API using an object instead of array:
const router = createMappedRouter({
home: {
path: '/',
element: <HomePage />
},
userProfile: {
path: '/user/:userId',
element: <UserProfile />
}
} as const);
usePaths(routePath)Get typed path parameters from the current route.
useQueryString(routePath)Get typed query string parameters from the current route.
useNavigation()Get navigation methods (push, replace, back, forward).
useDataLoader()Get data from the route loader with full type inference.
useFormActions()Get form action state (loading, result, etc.).
useErrorPage()Get any route errors that occurred.
useLoadingState()Check if route is currently loading.
<Brouther>The main provider component that enables routing.
<Outlet>Renders the matched route element.
<Link>Type-safe link component with automatic parameter validation.
<Form>Type-safe form component that integrates with route actions.
<Redirect>Declarative redirect component.
as constThis is crucial for TypeScript to infer literal types:
// ✅ Good
const router = createRouter([...] as const);
// ❌ Bad - loses type information
const router = createRouter([...]);
Keep your router definition in a single file and export it:
// router.ts
export const router = createRouter([...] as const);
export const { links, link, useQueryString, usePaths } = router;
Define query string types in your routes for better safety:
// Instead of handling raw strings
const searchParams = new URLSearchParams(location.search);
const page = parseInt(searchParams.get("page") || "1");
// Use Brouther's typed approach
const { page = 1 } = useQueryString(router.links.products);
// page is already a number!
Your IDE will autocomplete route names, parameters, and query strings. Use this to explore available options and catch errors early.
// React Router
<Route path="/user/:id" element={<User />} />
<Link to={`/user/${userId}`}>Profile</Link>
// Brouther
{
id: 'user',
path: '/user/:id',
element: <User />
}
<Link href={router.links.user} paths={{ id: userId }}>Profile</Link>
// Next.js
<Link href={`/user/${userId}?tab=posts`}>Posts</Link>
// Brouther
<Link
href={router.links.user}
paths={{ userId }}
query={{ tab: 'posts' }}
>Posts</Link>
Check out our examples directory for complete applications:
We welcome contributions! Please see our Contributing Guide for details.
MIT © Brouther Contributors
FAQs
A type-safe router for React applications that puts TypeScript first, ensuring your routes, parameters, and query strings are always in sync with your code.
The npm package brouther receives a total of 32 weekly downloads. As such, brouther popularity was classified as not popular.
We found that brouther demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.