Eddy Nguyen
Types of routing
Case study
Problems
Solution
How hard can routing be"
How hard can routing be"
- me
- me
How hard can routing be... WTF?"
Default behaviour
Based on HTML anchor's href
Page refreshes
Browser receives a new document
Handled by framework or library e.g. react-router or NextJS
URL is updated
Page does not refresh
Part or full page may change
Client maintains data & state
Hi! I'm Sam!
https://doge.com.vm
/
/about
reverse proxy
/app/login
/app/signup
https://doge.com.vm
https://doge.com.vm
/
/about
reverse proxy
internet magic
/app/login
/app/signup
https://doge.com.vm
https://doge.com.vm
Server side routing
/
/about
reverse proxy
internet magic
/app/login
/app/signup
https://doge.com.vm
https://doge.com.vm
/
/about
reverse proxy
internet magic
/app/login
/app/signup
https://doge.com.vm
https://doge.com.vm/about
Server side routing
/
/about
reverse proxy
internet magic
/app/login
/app/signup
https://doge.com.vm
https://doge.com.vm/app/login
Client side routing
https://doge.com.vm/app/signup
/
/about
reverse proxy
internet magic
/app/login
/app/signup
https://doge.com.vm
App
react-router
/login /signup /me /users/:id
/ /about
/login /signup /me /users/:id
App
react-router
"/" and "/about" should served by NextJS app for SEO reasons/ /about
NextJS
SEO
"/"
"/about"
NextJS
/login /signup /me /users/:id
Main App
react-router
/ /about
SEO App
/ /about
NextJS
/login /signup /me /users/:id
Main App
react-router
SEO App
/ /about
NextJS
/login /signup /me /users/:id
Main App
react-router
SEO App
/ /about
NextJS
/app/login /app/signup /app/me /app/users/:id
Main App
react-router
SEO App
// react-router
import { Link, Route, Switch } from 'react-router-dom';
const Routes = () =>
<Switch>
<Route path='/login' component={Login} />
<Route path='/signup' component={Signup} />
<Route path='/me' component={Me} />
<Route path='/users/:id' component={UserInfo} />
<Route path='/about' component={About} />
<Route exact path='/' component={Home} />
<Route component={PageNotFound} />
</Switch>
const Header = () =>
<>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
<Link to='/login'>Login</Link>
<Link to='/signup'>Signup</Link>
<Link to='/users/100?from=header'>User 100 info</Link>
</>// react-router
import { Link, Route, Switch } from 'react-router-dom';
const Routes = () =>
<Switch>
<Route path='/login' component={Login} />
<Route path='/signup' component={Signup} />
<Route path='/me' component={Me} />
<Route path='/users/:id' component={UserInfo} />
<Route path='/about' component={About} />
<Route exact path='/' component={Home} />
<Route component={PageNotFound} />
</Switch>
const Header = () =>
<>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
<Link to='/login'>Login</Link>
<Link to='/signup'>Signup</Link>
<Link to='/users/100?from=header'>User 100 info</Link>
</>// react-router
import { Link, Route, Switch } from 'react-router-dom';
const Routes = () =>
<Switch>
<Route path='/login' component={Login} />
<Route path='/signup' component={Signup} />
<Route path='/me' component={Me} />
<Route path='/users/:id' component={UserInfo} />
<Route path='/about' component={About} />
<Route exact path='/' component={Home} />
<Route component={PageNotFound} />
</Switch>
const Header = () =>
<>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
<Link to='/login'>Login</Link>
<Link to='/signup'>Signup</Link>
<Link to='/users/100?from=header'>User 100 info</Link>
</>Prepend '/app'
// react-router
import { Link, Route, Switch } from 'react-router-dom';
const Routes = () =>
<Switch>
<Route path='/login' component={Login} />
<Route path='/signup' component={Signup} />
<Route path='/me' component={Me} />
<Route path='/users/:id' component={UserInfo} />
<Route path='/about' component={About} />
<Route exact path='/' component={Home} />
<Route component={PageNotFound} />
</Switch>
const Header = () =>
<>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
<Link to='/login'>Login</Link>
<Link to='/signup'>Signup</Link>
<Link to='/users/100?from=header'>User 100 info</Link>
</>Remove '/about' and '/'
// react-router
import { Link, Route, Switch } from 'react-router-dom';
const Routes = () =>
<Switch>
<Route path='/login' component={Login} />
<Route path='/signup' component={Signup} />
<Route path='/me' component={Me} />
<Route path='/users/:id' component={UserInfo} />
<Route path='/about' component={About} />
<Route exact path='/' component={Home} />
<Route component={PageNotFound} />
</Switch>
const Header = () =>
<>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
<Link to='/login'>Login</Link>
<Link to='/signup'>Signup</Link>
<Link to='/users/100?from=header'>User 100 info</Link>
</>Prepend '/app'
// react-router
import { Link, Route, Switch } from 'react-router-dom';
const Routes = () =>
<Switch>
<Route path='/app/login' component={Login} />
<Route path='/app/signup' component={Signup} />
<Route path='/app/me' component={Me} />
<Route path='/app/users/:id' component={UserInfo} />
<Route component={PageNotFound} />
</Switch>
const Header = () =>
<>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
<Link to='/app/login'>Login</Link>
<Link to='/app/signup'>Signup</Link>
<Link to='/app/users/100?from=header'>User 100 info</Link>
</> // react-router
import { Link, Route, Switch } from 'react-router-dom';
const Routes = () =>
<Switch>
<Route path='/app/login' component={Login} />
<Route path='/app/signup' component={Signup} />
<Route path='/app/me' component={Me} />
<Route path='/app/users/:id' component={UserInfo} />
<Route component={PageNotFound} />
</Switch>
const Header = () =>
<>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
<Link to='/app/login'>Login</Link>
<Link to='/app/signup'>Signup</Link>
<Link to='/app/users/100?from=header'>User 100 info</Link>
</> There's a BUG!
// react-router
import { Link, Route, Switch } from 'react-router-dom';
const Routes = () =>
<Switch>
<Route path='/app/login' component={Login} />
<Route path='/app/signup' component={Signup} />
<Route path='/app/me' component={Me} />
<Route path='/app/users/:id' component={UserInfo} />
<Route component={PageNotFound} />
</Switch>
const Header = () =>
<>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
<Link to='/app/login'>Login</Link>
<Link to='/app/signup'>Signup</Link>
<Link to='/app/users/100?from=header'>User 100 info</Link>
</> // react-router
import { Link, Route, Switch } from 'react-router-dom';
const Routes = () =>
<Switch>
<Route path='/app/login' component={Login} />
<Route path='/app/signup' component={Signup} />
<Route path='/app/me' component={Me} />
<Route path='/app/users/:id' component={UserInfo} />
<Route component={PageNotFound} />
</Switch>
const Header = () =>
<>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
<Link to='/app/login'>Login</Link>
<Link to='/app/signup'>Signup</Link>
<Link to='/app/users/100?from=header'>User 100 info</Link>
</> "Link" is client side. "to" prop
// react-router
import { Link, Route, Switch } from 'react-router-dom';
const Routes = () =>
<Switch>
<Route path='/app/login' component={Login} />
<Route path='/app/signup' component={Signup} />
<Route path='/app/me' component={Me} />
<Route path='/app/users/:id' component={UserInfo} />
<Route component={PageNotFound} />
</Switch>
const Header = () =>
<>
<a href='/'>Home</a>
<a href='/about'>About</a>
<Link to='/app/login'>Login</Link>
<Link to='/app/signup'>Signup</Link>
<Link to='/app/users/100?from=header'>User 100 info</Link>
</>"a" is server side. "href" prop
Manually find and replace routing logic (server side vs client side)
Manually update "href" or "to" prop
Path patterns are repeated multiple times e.g '/app/users/:id' and '/app/users/100'
Manually find and replace routing logic (server side vs client side)
Manually update "href" or "to" prop
Path patterns are repeated multiple times e.g '/app/users/:id' and '/app/users/100'
Abstract client vs server side routing
Abstract path pattern from Routes and Links
Abstract static parts of URL when creating links e.g. '/app/users/:id'
/app/users/
Manually find and replace routing logic (server side vs client side)
Manually update "href" or "to" prop
Path patterns are repeated multiple times e.g '/app/users/:id' and '/app/users/100'
Abstract client vs server side routing
Abstract path pattern from Routes and Links
Abstract static parts of URL when creating links e.g. '/app/users/:id'
/app/users/
Links share same API, handles routing internally
Path patterns built into Routes and Links
Pass dynamic parts of pattern e.g. '/app/users/:id'
:id
Abstract client vs server side routing
Abstract path pattern from Routes and Links
Abstract static parts of URL when creating links e.g. '/app/users/:id'
/app/users/
// react-router
import { Link, Route, Switch } from 'react-router-dom';
const Routes = () =>
<Switch>
<Route path='/app/login' component={Login}/>
<Route path='/app/users/:id' component={UserInfo} />
</Switch>
const Header = () =>
<>
<a href='/'>Home</a>
<Link to='/app/login'>Login</Link>
<Link to='/app/users/100?from=header'>User 100 info</Link>
</>// NextJS
import Link from 'next/link';
const Header = () =>
<>
<Link href='/'>
<a>Home</a>
</Link>
<a href='/app/login'>Login</a>
<a href='/app/users/100?from=header'>User 100 info</a>
</>/app/login
/app/users/:id
/ (home)
// react-router - client side routing
import Link from 'react-router-dom';
const pattern = '/app/users/:id'; // Path pattern is declared once
const generateUrl = (pattern, path, urlQuery) => {
// pattern: /app/users/:id
// inputParams: { id: '100' }
// urlQuery: { from: 'header' }
// returns '/app/users/100?from=header'
}
const RouteToUserInfo = {
pattern,
Link: ({ path, urlQuery, ...props }) => {
const to = generateUrl(pattern, path, urlQuery);
return <Link {...props} to={to} />
},
}// NextJS - server side routing
const pattern = '/app/users/:id';
const generateUrl = (pattern, path, urlQuery) => {
...
}
const RouteToUserInfo = {
pattern,
Link: ({ path, urlQuery, ...props }) => {
const to = generateUrl(pattern, path, urlQuery);
return <a {...props} href={to} />
},
}/app/login
/app/users/:id
/ (home)
RouteToUserInfo
// react-router - client side routing
import Link from 'react-router-dom';
const pattern = '/app/users/:id'; // Path pattern is declared once
const generateUrl = (pattern, path, urlQuery) => {
// pattern: /app/users/:id
// inputParams: { id: '100' }
// urlQuery: { from: 'header' }
// returns '/app/users/100?from=header'
}
const RouteToUserInfo = {
pattern,
Link: ({ path, urlQuery, ...props }) => {
const to = generateUrl(pattern, path, urlQuery);
return <Link {...props} to={to} />
},
}// NextJS - server side routing
const pattern = '/app/users/:id';
const generateUrl = (pattern, path, urlQuery) => {
...
}
const RouteToUserInfo = {
pattern,
Link: ({ path, urlQuery, ...props }) => {
const to = generateUrl(pattern, path, urlQuery);
return <a {...props} href={to} />
},
}/app/login
/app/users/:id
/ (home)
RouteToUserInfo
// react-router - server side routing
const pattern = '/';
const generateUrl = (pattern, inputParams, urlQuery) => {
...
}
const RouteToHome = {
pattern,
Link: ({ path, urlQuery, ...props }) => {
const to = generateUrl(pattern, path, urlQuery);
return <a {...props} href={to} />
},
}// NextJS - server side routing
import Link from 'next/link';
const pattern = '/';
const generateUrl = (pattern, inputParams, urlQuery) => {
...
}
const RouteToHome = {
pattern,
Link: ({ path, urlQuery, ...props }) => {
const to = generateUrl(pattern, path, urlQuery);
return <Link {...props} href={to} />
},
}/app/login
/app/users/:id
/ (home)
RouteToHome
// react-router - server side routing
const pattern = '/';
const generateUrl = (pattern, inputParams, urlQuery) => {
...
}
const RouteToHome = {
pattern,
Link: ({ path, urlQuery, ...props }) => {
const to = generateUrl(pattern, path, urlQuery);
return <a {...props} href={to} />
},
}// NextJS - server side routing
import Link from 'next/link';
const pattern = '/';
const generateUrl = (pattern, inputParams, urlQuery) => {
...
}
const RouteToHome = {
pattern,
Link: ({ path, urlQuery, ...props }) => {
const to = generateUrl(pattern, path, urlQuery);
return <Link {...props} href={to} />
},
}/app/login
/app/users/:id
/ (home)
RouteToHome
// react-router
import RouteToHome from 'main/src/routes/RouteToHome';
import RouteToLogin from 'main/src/routes/RouteToLogin';
import RouteToUserInfo from 'main/src/routes/RouteToUserInfo';
const Routes = () =>
<Switch>
<Route path={RouteToLogin.pattern} component={Login}/>
<Route path={RouteToUserInfo.pattern} component={UserInfo} />
</Switch>
const Header = () =>
<>
<RouteToHome.Link>Home</RouteToHome.Link>
<RouteToLogin.Link>Login</RouteToLogin.Link>
<RouteToUserInfo.Link path={{ id: '100' }} urlQuery={{ from: 'header' }}>
User 100 info
</RouteToUserInfo.Link>
</> // NextJS
import RouteToHome from 'seo/src/routes/RouteToHome';
import RouteToLogin from 'seo/src/routes/RouteToLogin';
import RouteToUserInfo from 'seo/src/routes/RouteToUserInfo';
const Header = () =>
<>
<RouteToHome.Link>Home</RouteToHome.Link>
<RouteToLogin.Link>Login</RouteToLogin.Link>
<RouteToUserInfo.Link path={{ id: '100' }} urlQuery={{ from: 'header' }}>
User 100 info
</RouteToUserInfo.Link>
</>Every Link has one destination, rather than a Link which can go to any destination"
Same API regardless of routing library / framework
Route patterns are declared once per app
href are generated automatically
Boilerplate-y
Route patterns are declared once per app
Code client vs server side routing
Generate route objects
Single source of truth for route patterns
Handle client vs server side routing automatically
Generate route objects
Single source of truth for route patterns
Handle client vs server side routing automatically
Generate route objects
Single source of truth for route patterns
Handle client vs server side routing automatically
Bonus: Convenient functions and types
At project root:
$ npm i --save-dev route-codegen
In each app:
$ npm i route-codegen
// route-codegen.yml
apps:
main:
routes:
login: /app/login
signup: /app/signup
me: /app/me
userInfo: /app/users/:id/:subview(profile|pictures)?
routingType: ReactRouter
destinationDir: main/src/routes
seo:
routes:
home: /
about: /about
routingType: NextJS
destinationDir: seo/src/routes
backend:
generateLink: false
destinationDir: backend/src/routes
otherApps:
routes:
books: /legacy/books
report: /legacy/report
$ npx route-codegen
At project root:
// RouteToHome - server side routing
import { generateUrl } from 'route-codegen';
import React from 'react';
const pattern = '/';
type OmittedLinkProps = Omit<
React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>,
'href'
>;
interface UrlParts {
urlQuery?: Partial<Record<string, string>>;
}
type RouteLinkProps = OmittedLinkProps & UrlParts;
interface DefaultRoute {
pattern: string;
generate: (urlParts: UrlParts) => string;
Link: React.FunctionComponent<RouteLinkProps>;
}
const RouteToHome: DefaultRoute = {
pattern,
generate: ({ urlQuery }) => generateUrl(pattern, {}, urlQuery),
Link: function RouteLink({ urlQuery, ...props }) {
const to = generateUrl(pattern, {}, urlQuery);
return <a href={to} {...props} />;
}
};// RouteToHome - client side routing - NextJS
import { generateUrl } from 'route-codegen';
import React from 'react';
import Link, { LinkProps as OriginalLinkProps } from 'next/link';
const pattern = '/';
type OmittedLinkProps = Omit<OriginalLinkProps, 'href'>;
interface UrlParts {
urlQuery?: Partial<Record<string, string>>;
}
type RouteLinkProps = OmittedLinkProps & UrlParts;
interface NextJSRoute {
pattern: string;
generate: (urlParts: UrlParts) => string;
Link: React.FunctionComponent<RouteLinkProps>;
}
const RouteToHome: NextJSRoute = {
pattern,
generate: ({ urlQuery }) => generateUrl(pattern, {}, urlQuery),
Link: function RouteLink({ urlQuery, ...props }) {
const to = generateUrl(pattern, {}, urlQuery);
return <Link href={to} {...props} />;
}
};
export default RouteToHome;
// RouteToUserInfo - client side routing - react-router
import { generateUrl } from 'route-codegen';
import React from 'react';
import { Link, LinkProps as OriginalLinkProps } from 'react-router-dom';
// ... other imports
const pattern = '/app/users/:id/:subview(profile|pictures)?';
// ... other interfaces
export interface RouteToUserInfoPathParams {
id: string;
subview?: 'profile' | 'pictures';
}
interface UrlParts<P> {
path: P;
urlQuery?: Partial<Record<string, string>>;
}
interface ReactRouterRoute<P> {
pattern: string;
generate: (urlParts: UrlParts<P>) => string;
Link: React.FunctionComponent<RouteLinkProps<P>>;
// ... other convenient functions
}
const RouteToUserInfo: ReactRouterRoute<RouteToUserInfoPathParams> = {
pattern,
generate: ({ path, urlQuery }) => generateUrl(pattern, path, urlQuery),
Link: function RouteLink({ path, urlQuery, ...props }) {
const to = generateUrl(pattern, path, urlQuery);
return <Link {...props} to={to} />;
},
// ... other convenient functions
};
// route-codegen.yml
apps:
main:
routes:
login: /app/login
signup: /app/signup
me: /app/me
userInfo: /app/users/:id
routingType: ReactRouter
destinationDir: main/src/routes
reactRouterLinkOptions:
path: common/components/Link
propsInterfaceName: LinkProps
hrefProp: to
defaultLinkOptions:
path: common/ui/Anchor
propsInterfaceName: AnchorProps
hrefProp: href
seo:
routes:
home: /
about: /about
routingType: NextJS
destinationDir: seo/src/routes
nextJSLinkOptions:
path: src/common/components/Link
propsInterfaceName: LinkProps
hrefProp: href
defaultLinkOptions:
path: src/common/ui/Anchor
propsInterfaceName: AnchorProps
hrefProp: hrefimport { generateUrl } from 'route-codegen';
import React from 'react';
import Link, { LinkProps as OriginalLinkProps } from 'src/common/components/Link';
const pattern = '/';
type OmittedLinkProps = Omit<OriginalLinkProps, 'href'>;
interface UrlParts {
urlQuery?: Partial<Record<string, string>>;
}
type RouteLinkProps = OmittedLinkProps & UrlParts;
interface NextJSRoute {
pattern: string;
generate: (urlParts: UrlParts) => string;
Link: React.FunctionComponent<RouteLinkProps>;
}
const RouteToHome: NextJSRoute = {
pattern,
generate: ({ urlQuery }) => generateUrl(pattern, {}, urlQuery),
Link: function RouteLink({ urlQuery, ...props }) {
const to = generateUrl(pattern, {}, urlQuery);
return <Link href={to} {...props} />;
}
};
export default RouteToHome;- me
How hard can routing be... WTF?"
How hard can routing be"
- me
Software Engineer
Website: Github: Route codegen: LinkedIn:
https://eddy.works https://github.com/eddeee888 https://github.com/eddeee888/route-codegen https://www.linkedin.com/in/eddeee