Routing in modern web apps
Eddy Nguyen

Overview
-
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?"
Types of routing
Server side routing
-
Default behaviour
-
Based on HTML anchor's href
-
Page refreshes
-
Browser receives a new document

Client side routing
-
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
Case study
Initial routes
App
react-router
/login /signup /me /users/:id
/ /about
Initial routes

/login /signup /me /users/:id
App
react-router
"/" and "/about" should served by NextJS app for SEO reasons/ /about
NextJS
SEO
"/"
"/about"
Updated routes
NextJS
/login /signup /me /users/:id
Main App
react-router
/ /about
SEO App
Updated routes
/ /about
NextJS
/login /signup /me /users/:id
Main App
react-router
SEO App
Updated routes
/ /about
NextJS
/login /signup /me /users/:id
Main App
react-router
SEO App
Updated routes
/ /about
NextJS
/app/login /app/signup /app/me /app/users/:id
Main App
react-router
SEO App
Initial code
// 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>
</>Refactor
// 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>
</>Refactor
// 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'
Refactor
// 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 '/'
Refactor
// 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>
</> Refactor
// 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>
</> Refactor

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>
</> Refactor (cont.)
// 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>
</> Refactor (cont.)
"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>
</>Refactor (cont.)
"a" is server side. "href" prop
Problems
-
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'
Finding a solution
Problems
-
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'
Requirements
-
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'
Requirements
-
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/
Ideas
-
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>
</> Final Syntax
// 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"
Pros
-
Same API regardless of routing library / framework
-
Route patterns are declared once per app
-
href are generated automatically
Room for improvements
-
Boilerplate-y
-
Route patterns are declared once per app
-
Code client vs server side routing
Improvements
What if we could...
-
Generate route objects
-
Single source of truth for route patterns
-
Handle client vs server side routing automatically
What if we could...
-
Generate route objects
-
Single source of truth for route patterns
-
Handle client vs server side routing automatically
What if we could...
-
Generate route objects
-
Single source of truth for route patterns
-
Handle client vs server side routing automatically
-
Bonus: Convenient functions and types
Folder structure

Installation
At project root:
$ npm i --save-dev route-codegen
In each app:
$ npm i route-codegen
Configuration
// 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
Generate routes
$ npx route-codegen
At project root:
Generated files

Route object - Server side
// 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;
Route object - NextJS
// 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 object - react-router
Features
Using customised links
// 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;Using customised links
Type support - Link

Type support - useParams


Type support - useParams


... more features
coming soon!
- me
How hard can routing be... WTF?"
How hard can routing be"
- me
Questions
We are hiring!
Software Engineer
Thank you!
Website: Github: Route codegen: LinkedIn:
https://eddy.works https://github.com/eddeee888 https://github.com/eddeee888/route-codegen https://www.linkedin.com/in/eddeee
Routing in modern web apps
By Eddy Nguyen
Routing in modern web apps
Routing is the concept that connects everything on the internet. What are the challenges that we may encounter when developing web apps? Let's understand the problems and explore potential solutions
- 243