diff --git a/docs/api.md b/docs/api.md
index e3e55ea..3914747 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -33,6 +33,61 @@ Where `action` is just a regular function that may, or may not, return any arbit
— a string, a React component, anything!
+## `href(routes, routeName[, routeParams])` ⇒ `String|null`
+
+Traverses the list of routes in the order they are defined until it finds the first route that
+matches provided name string.
+
+```js
+// ./routes/index.js
+import { href } from 'universal-router';
+
+const routes = {
+ path: '/',
+ children: [
+ { name: 'one', path: '/one', action: () => {} },
+ { path: '/two', action: () => {}, children: [
+ { path: '/three', action: () => {} }
+ { name: 'four', path: '/four/:four', action: () => {} }
+ ] }
+ ]
+};
+
+export default routes;
+
+console.log(href(routes, 'one'));
+// => /one
+
+console.log(href(routes, 'three'));
+// => null
+
+console.log(href(routes, 'four', { four: 'a' }));
+// => /two/four/a
+```
+
+```js
+// ./components/Link/index.js
+import React from 'react';
+import { href } from 'universal-router';
+import routes from '../../routes';
+
+const Link = ({ routeName, routeParams, children }) => (
+ {children}
+);
+
+// ./components/Navigation.js
+import React from 'react';
+import Link from './components/Link';
+
+const Navigation = () => (
+
+);
+```
+
+
## Nested Routes
Each route may have an optional `children: [ ... ]` property containing the list of child routes:
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 2ae1b5b..746189e 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -54,3 +54,26 @@ resolve(routes, { path: '/one' }).then(component => {
// renders:
Page One
});
```
+
+## Use href method to generate a path by name
+
+```js
+import React from 'react';
+import { href } from 'universal-router';
+
+const routes = [
+ { name: 'one', path: '/one', action: () => Page One
},
+ { name: 'two', path: '/two/:two', action: () => Page Two
},
+ { path: '*', action: () => Not Found
}
+];
+
+const LinkOne = () => (
+ LinkOne
+);
+// LinkOne
+
+const LinkTwo = () => (
+ LinkTwo
+);
+// LinkTwo
+```
diff --git a/src/href.js b/src/href.js
new file mode 100644
index 0000000..36cbb4f
--- /dev/null
+++ b/src/href.js
@@ -0,0 +1,44 @@
+/**
+ * Universal Router (https://www.kriasoft.com/universal-router/)
+ *
+ * Copyright © 2015-2016 Konstantin Tarkus, Kriasoft LLC. All rights reserved.
+ *
+ * This source code is licensed under the Apache 2.0 license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import toRegExp from 'path-to-regexp';
+
+function resolvePath(route, routeName) {
+ const routePath = (route.path === '/') ? '' : route.path;
+
+ if (route.name && route.name === routeName) {
+ return routePath;
+ }
+
+ if (route.children) {
+ for (let i = 0; i < route.children.length; i += 1) {
+ let resultPath = resolvePath(route.children[i], routeName);
+
+ if (resultPath !== null) {
+ resultPath = routePath + resultPath;
+ return resultPath.startsWith('/') ? resultPath : `/${resultPath}`;
+ }
+ }
+ }
+
+ return null;
+}
+
+function href(routes, routeName, routeParams = {}, options = { pretty: false }) {
+ const root = Array.isArray(routes) ? { path: '/', children: routes } : routes;
+ const path = resolvePath(root, routeName);
+
+ if (path === null) {
+ return null;
+ }
+
+ return toRegExp.compile(path)(routeParams, options);
+}
+
+export default href;
diff --git a/src/main.js b/src/main.js
index d34d890..557b050 100644
--- a/src/main.js
+++ b/src/main.js
@@ -8,6 +8,7 @@
*/
import resolve from './resolve';
+import href from './href';
-export { resolve };
-export default { resolve };
+export { resolve, href };
+export default { resolve, href };
diff --git a/test/href.spec.js b/test/href.spec.js
new file mode 100644
index 0000000..6c99354
--- /dev/null
+++ b/test/href.spec.js
@@ -0,0 +1,90 @@
+/**
+ * Universal Router (https://www.kriasoft.com/universal-router/)
+ *
+ * Copyright © 2015-2016 Konstantin Tarkus, Kriasoft LLC. All rights reserved.
+ *
+ * This source code is licensed under the Apache 2.0 license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import { expect } from 'chai';
+import { href } from '../src/main';
+
+const emptyRoutes = [];
+const simpleRoutes = [{ name: 'root', path: '/', children: [{ name: 'a', path: '/a' }] }];
+const nestRoutes = [{
+ name: 'root',
+ path: '/',
+ children: [
+ { name: 'a', path: '/a/:a' },
+ { name: 'b', path: '/b/:b?' },
+ {
+ name: 'c',
+ path: '/c/:c(\\d+)?',
+ children: [
+ {
+ name: 'd',
+ path: '/d',
+ children: [
+ { name: 'f', path: '/f/:f' },
+ ],
+ },
+ ],
+ },
+ { name: 'a', path: '/never-mounted-by-name' },
+ ],
+}];
+
+describe('href(routes, routeName, routeParams, [options = { pretty: false }])', () => {
+
+ it('should return false if no route found', async () => {
+ let path;
+
+ path = href(emptyRoutes, 'z');
+ expect(path).to.be.null;
+
+ path = href(simpleRoutes, 'z');
+ expect(path).to.be.null;
+
+ path = href(nestRoutes, 'z');
+ expect(path).to.be.null;
+ });
+
+ it('should find and mount the first route by name', () => {
+ let path;
+
+ path = href(simpleRoutes, 'root');
+ expect(path).to.be.equal('/');
+
+ path = href(nestRoutes, 'root');
+ expect(path).to.be.equal('/');
+
+ path = href(nestRoutes, 'a', { a: 'a' });
+ expect(path).to.be.equal('/a/a');
+
+ path = href(nestRoutes, 'b');
+ expect(path).to.be.equal('/b');
+
+ path = href(nestRoutes, 'b', { b: 'b' });
+ expect(path).to.be.equal('/b/b');
+
+ path = href(nestRoutes, 'c');
+ expect(path).to.be.equal('/c');
+
+ path = href(nestRoutes, 'c', { c: '1' });
+ expect(path).to.be.equal('/c/1');
+
+ path = href(nestRoutes, 'd');
+ expect(path).to.be.equal('/c/d');
+
+ path = href(nestRoutes, 'd', { c: '1' });
+ expect(path).to.be.equal('/c/1/d');
+
+ path = href(nestRoutes, 'f', { f: 'f' });
+ expect(path).to.be.equal('/c/d/f/f');
+
+ path = href(nestRoutes, 'f', { c: '1', f: 'f' });
+ expect(path).to.be.equal('/c/1/d/f/f');
+ });
+
+});