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: href
import { 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

Made with Slides.com