RouterJS is a simple and powerful javascript router. It's simple to use, versatile and ready to be coupled with your framework of choice. It can work in the browser or on native applications.
🌟 Reasonable defaults. Just define your routes and go.
🖱️ Smart listener. Don't define custom links, RouterJS is able to understand what click to handle!
⚙️ Works everywhere: thought for the browser, it can also run on native apps or on your watch, drop in your engine.
🐞 Type safe. RouterJS is written in Typescript and fully tested.
🌍 Plain javascript. It can be used as is or with any library you prefer (React, Angular, Vue...)
-
- 4.1. Matching params
- 4.2. Query params
- 4.3. "req.get" - One method to get them all
- 4.4. Regexp and splats
- 4.5. Multiple matching routes
- 4.6. Middlewares
- 4.7. Always callbacks
- 4.8. Exit handlers
- 4.9. Context
- 4.10. Errors
- 4.11. Engines
- 4.11.1. BrowserHistoryEngine
- 4.12. Request object
- 4.13. Options
- 4.14. Router methods
Version 2 of this router represents a complete rewrite and redefines some concept of the older versions. Refer to the migration guide if you were using one of those versions.
Are you looking for documentation of version 1? Check it out here.
This library is available on npm
npm install routerjs
If you're looking for a way to integrate this library in your React application, have a look at react-routerjs
To define your routes simply match a route url with a callback
import { createRouter } from 'routerjs';
// Create the instance of your router
const router = createRouter()
// Define the route matching a path with a callback
.get('/user', (req, context) => {
// Handle the route here...
})
// Calling "run" will execute handlers immediately for the current url.
// You can avoid calling it and wait for the next route change instead.
.run();
path: The path can be a string, like the one above, or a regular expression
callback: The callback can be synchronous or asynchronous and it receives two parameters:
- req, the current request, we'll see it in details later.
- context, contains some fixed informations, as explained here.
When you create a router with the default engine, any click on anchors will be intercepted and converted to a router event. You can discover more on the default engine and how to opt-out of this click behavior here.
Under the hood the path matching is done through path-to-regexp, so you can look at its documentation to know all the possibilities.
A route can define several named params in the form :name
that will be available inside the request through req.params
const router = createRouter()
.get('/user/:id', async (req, context) => {
const user = await getUser(req.params.id);
});
This route will match, for example, the route /user/123
and the id
will be 123
.
All the params are strings.
Multiple params can be used together
const router = createRouter()
.get('/post/:date/:title', async (req, context) => {
// ...
});
Again: look at the documentation of path-to-regexp to know about all of the features:
- Optional parameters
/user/:id?
- Zero or more
/posts/:date*
- One or more
/posts/:date+
- Unnamed parameters
/:foo/(.*)
- Custom matching parameters
/user/:id(\\d+)
Any regular query parameter can be retrieved through req.query
const router = createRouter()
.get('/users', async (req, context) => {
const filter = req.query.filter || 'all';
});
So the path /users?filter=active
will result in filter to be active
The req.get
method looks for parameters in the params, then in the query and otherwise it fallbacks to a default value if one is provided.
// Visiting /users/john?age=25
router
.get('/users/:username', (req, context) => {
const username = req.get('username'); // will be 'john' because is found in params
const age = req.get('age', 18); // will be 25 because is found in query
const surname = req.get('surname', 'Snow');// will be 'Snow' because of provided default value
const address = req.get('address'); // will be undefined
});
A route can be defined through a regular expression instead of a string. Any capturing group value can be retrieved from the req.splats
array
router
.get(/\/foo\/bar\/?(.*)/i, (req, context) => {
console.log(req.splats)
});
The path /foo/bar/something
will match and the output will be
['something']
because the splats will contain the value from the caturing group in the regular expression.
NOTE in the future, named capturing group will be used to get regular params through regular expressions.
All the matching routes are executed
// For path "/users/admin"
router
.get('/users/:name', () => {
console.log('I am called');
})
.get('/users/admin', () => {
console.log('I am called too');
});
For the path /users/admin
, both routes will be exectued because they both match. If you want to prevent this, you need to call req.stop()
router
.get('/users/:name', (req, context) => {
//...
req.stop();
})
.get('/users/admin', (req, context) => {
// this won't be called because req is stopped
});
You can write middlewares which are functionalities to run before your route handler.
Implementation of middlewares are inspired by the ones in zeit micro, so they're simply composition of functions!
const logMiddleware = fn => (req, context) => {
console.log(`${Date.now()} - GET ${context.path}`);
};
router
.get('/users', logMiddleware((req, context) => {
// ...
}))
Now every route match will be logged.
To compose together more middlewares, simply use the compose
function.
import { createRouter, compose } from 'routerjs';
const userRoute = (req, context) => {
//...
};
const router = createRouter()
.get(
'/users',
compose(
logMiddleware,
authMiddleware,
// any other middleware
)(userRoute)
);
If you just have middlewares but not an handler, you can use pipe instead.
import { createRouter, pipe } from 'routerjs';
const userRoute = (req, context) => {
//...
};
const router = createRouter()
.get(
'/users',
pipe(
logMiddleware,
authMiddleware,
// any other middleware
)
);
You can define a callback that is executed always, on every route and despite the fact that the request has been stopped or not.
router
.get('/users')
.get('/posts')
.get('/post/:id')
.always((context) => {
// this will be executed for every route.
// Context contains at least the `path`
console.log('Path is: ', context.path)
});
always
callbacks receive only the context
as parameter.
If we navigate to /post/14
, this will be logged to the console
Path is: /post/14
You can attach handlers that are executed when the user leaves a route. The syntax is the same as get
and the callback receives the same arguments
router
.get('/', () => {})
.exit('/', (req, context) => {
// ... do something when the route "/" is left
});
The behavior is the same as for get
and so you can stop the execution and populate the context
. Let's say that you have a series of get
s, that run when the user enters a route, and a series of exit
s that run when the user leaves the route.
The context is an object which is retained through the execution of each callback.
It contains the current path
but you can attach whatever you want. In this example we'll use a middleware to populate the context with some user information
const userInfo = fn => async (req, context) => {
// This fake method get the user from a hypotetical JWT token
// contained in the cookies, then attaches it to the context
context.user = await userFromJWT(document.cookies);
}
router
.get('/post/:id', compose(
userInfo
)((req, context) => {
// The context will contain those user info fetched before
const userId = context.user.id;
}))
.get('/post/*', (req, context) => {
// The context will have the user info here as well, since the context
// object is kept through each callback!
})
.always((context) => {
// also here, `context.user` is available
})
Your routes can throw errors for any reason and you can listen to those errors. Errors in RouterJS behave like http errors and so they have a code associated. You can add a listener for the code you prefer and, if an error has no associated code, it behaves like a 500
.
router
.get('/user/:id', () => {
// ...
throw new Error('User not present');
})
.error(500, (err, context) => {
// here you handle any 500 error
console.error(err);
});
If a route is not found, a 404 error is thrown and you can listen to those as well.
router
.get( /* ... */ )
.error(404, (err, context) => {
console.log(`Route ${context.path} not found`);
});
You can attach any statusCode
you want to your errors.
router
.get('/authorize', () => {
// ...
const e = new Error('Not authorized');
e.statusCode = 403;
throw e;
})
.error(403, (err, context) => {
// Your 403 errors are caught here.
});
You can also add an error handler that listen to any error just using the wildcard *
router
.get('/', () => {
// ...
})
.error('*', (err, context) => {
// This will catch any error, regardless of the statusCode
});
By default RouterJS will log for 404 and 500 errors but this behavior can be opt-out in the future.
RouterJS can work with several engines. For the moment only one engine exists and it's a browser engine that uses pushState
API under the hood. In the future there will be an engine that uses hashbang
instead and you can imagine engines for other environments that use javascipt but which are not a browser (node.js, native frameworks, etc...).
The current engine is setup automatically for you by RouterJS but you can pass your own instance creator.
import { createRouter, BrowserHistoryEngine} from 'routerjs';
const router = createRouter({engine: BrowserHistoryEngine({bindClick: false})});
In this example the BrowserHistoryEngine
won't automatically listen to click to anchors and it will be up to you to call the navigate
method when appropriate.
As said this engine works with the pushState
API. It takes some parameters:
- bindClick: If true any click on an anchor will be automatically translated to a router event. Default to true.
The clicks on anchors are listened unless:
- The anchor has a
data-routerjs-ignore
attribute - The anchor has a
download
attribute - The anchor has a
target
attribute - The anchor has a
rel="external"
attribute - The anchor href points to a different domain
Here the complete list of properties available in the request object, req
, the first parameter of the callbacks:
- path: The current path (cleaned)
- params: An object containing every matched param, if any.
- query: An object with the query params, if any.
- get: A function in the form
(k: string, defValue: any) => any
that looks for params - splats: An array containing any matching group for routes defined through regular expressions.
- stop: A function to avoid any following matched route to be executed
- isStopped: A function
() => boolean
that tells if stop has been called.
The router can be instantiated with several options:
- engine: A function that returns an instance of a custom engine, usually an external package.
- ignoreCase: If
true
that route will NOT check the case of the path when matching. Default to false. - basePath: A base path to ignore. If, for example, the basePath is
/blog
, any path will exclude that string. In this case/blog/post/:id
can be simply written as/post/:id
router = createRouter({basePath: '/blog'})
.get('/post/:id', () => {
// This will match `/blog/post/:id`
});
This can be handy when the router is for an application served from a subdir.
A list of methods available in the router:
- get: Function to define a route
- always: Function to define an handler for any route, even if the
req.stop
has been called - error: Function to define an error handler
- navigate: Function to navigate. i.e.
router.navigate('/post/3')
- setLocation: Same as navigate but the handlers are not run
- go: Navigate into the history of
n
positions.router.go(-2)
orrouter.go(1)
- back: Navigate back in the history
- forward: Navigate forward in the history
- run: Function that execute immediately the router, even if no route has changed yet.
This function is useful to run during the application to startup to immediately elaborate the handler for the current url. You can also pass apath
to the function and the url will be adjusted accordingly - teardown: Function to remove any event handler instantiated by the router. Useful to cleanup memory on application disposal.
- buildUrl: Function to get an url considering also the baseUrl.
i.e, if the base url is/blog
, the callrouter.buildUrl('/post')
will return/blog/post