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

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