DEV Community

loading...
Cover image for a first look at elder.js - routes

a first look at elder.js - routes

ajcwebdev profile image ANTHONY_CAMPOLO Updated on ・7 min read

At the core of any site are its routes or templates. To learn more about routing in Elder.js, open localhost:3000/simple in your browser.

01-simple-route

In Elder.js a route is made up of 2 files that live in your route folder: ./src/routes/${routeName}/.

1 - A route.js file that defines route details

// src/routes/simple/routes.js

module.exports = {
  all: async () => [{ slug: 'simple' }],

  permalink: ({ request }) => `/${request.slug}/`,

  data: async ({ request }) => {
    return {
      title: 'Elder.js Route: An Overview',

      steps: [
        `Step 1: All routes require a route.js and a svelte template. Look at ./src/simple/route.js to follow along.`,
        `Step 2: We define an 'all()' function that returns an array of 'request' objects.`,
        `Step 3: We define a permalink function that transforms the 'request' objects from 'all()' into permalinks.`,
        `Step 4: We define a data function that makes data available in your svelte template.`,
      ],

      content: `
      <h2>How Routing Works:</h2>
      <p>Elder.js's routing flow is different from what you'll see in other frameworks such as <span class="code">express</span>.</p>
      <p>Most frameworks define routes like so: <span class="code">/blog/:slug/</span>.</p>
      <p>Then when you visit <span class="code">/blog/simple/</span> that route would receive a <span class="code">request</span> object of <span class="code">{ slug: "simple" }</span>.</p>
      <p>While this approach works, a huge downside is that it forces a static site generator to crawl all of the links on a site to know all of the request objects.</p>
      <p>Since Elder.js is built for speed and crawling is expensive, Elder.js asks you to define all of your <span class="code">request</span> objects in your <span class="code">all()</span> function.</p>
      <p>Once it has your requests, it runs them through the <span class="code">permalink()</span> function to can build an entire map of your site so we don't have to crawl it but can generate it on the fly.</p>

      <h3>Learning Exercise: </h3>
      <p>Try adding <span class="code">{ slug: "another-request" }</span> to the <span class="code">all()</span> function in <span class="code">./src/simple/route.js</span> and then visit /another-request/ to see that you added another page with the same data.</p>
      `,
    };
  },

  // template: 'Simple.svelte' // this is auto-detected.
  // layout: 'Layout.svelte' // this is auto-detected.
};
Enter fullscreen mode Exit fullscreen mode
  • all - Returns an array of all of the request objects of a route.
all: async () => [{ slug: 'simple' }]
Enter fullscreen mode Exit fullscreen mode
  • permalink - Takes a request object and returns a relative permalink, in this case /simple/.
permalink: ({ request }) => `/${request.slug}/`
Enter fullscreen mode Exit fullscreen mode
  • data - Populates an object that will be available in our Svelte template under the data key.
data: async ({ request }) => {
  return {
    title: 'Elder.js Route: An Overview',
    steps: [ ... ],
    content: `
    ...
    `,
  };
},
Enter fullscreen mode Exit fullscreen mode

2 - A Svelte component matching the ${routeName} is used as a template (referred to as "Svelte Templates")

// src/routes/simple/Simple.svelte

<script>
  export let data, helpers;
</script>

<style>
  a {
    margin-bottom: 1rem;
    display: inline-block;
  }
</style>

<svelte:head>
  <title>{data.title}</title>
</svelte:head>

<a href="/">&LeftArrow; Home</a>

<h1>{data.title}</h1>

<ul>
  {#each data.steps as step}
    <li>{step}</li>
  {/each}
</ul>

{@html data.content}
Enter fullscreen mode Exit fullscreen mode

There is a learning exercise at the bottom of the page:

Try adding { slug: "another-request" } to the all() function in ./src/simple/route.js and then visit /another-request/ to see that you added another page with the same data.

all: async () => [
  { slug: 'simple' },
  { slug: 'another-request' }
],
Enter fullscreen mode Exit fullscreen mode

02-another-request

Explicit Routing

Elder.js uses "explicit routing" instead of the more common "parameter based" routing found in most frameworks like express. Elder.js asks you to define all of your request objects in your all() function.

Once it has your requests, it runs them through the permalink() function to build an entire map of your site. This means we don't have to crawl the site, instead we can generate it on the fly.

Here's an example of how you'd setup a route like /blog/:slug/ where there are only 2 blogposts.

// ./src/routes/blog/route.js

module.exports = {
  template: 'Blog.svelte',

  permalink: ({ request }) => `/blog/${request.slug}/`,

  all: async () => {
    return [
      { slug: 'blogpost-1' },
      { slug: 'blogpost-2' }
    ],
  },

  data: async ({ request }) => {
    return {
      blogpost: `This is the blogpost for the slug: ${request.slug}`.
    }
  };
Enter fullscreen mode Exit fullscreen mode

permalink()

Similar to standard route definitions seen with placeholders. /blog/:slug/ would be defined as /blog/${request.slug}/. It takes the request objects returned from all and transforms them into relative urls. Async is not supported.

permalink: ({ request, settings, helpers }): String => {

  // request: object received from all() function, passing a 'slug' parameter is recommended, but any naming can be used

  // settings: describes Elder.js bootstrap settings

  // helpers: helpers from `./src/helpers/index.js` file

  return String;
};
Enter fullscreen mode Exit fullscreen mode

all()

Returns an array of all of the request objects for a given route. Often this array may come from a data store. Since we're explicitly saying we only have 2 blog posts, only two pages will be generated.

all: async ({ settings, query, data, helpers }): Array<Object> => {

  // settings: describes Elder.js settings at initialization

  // query: empty object usually populated on the 'bootstrap' hook with a db or api connection that is sharable throughout all hooks, functions, and shortcodes

  // data: any data set on the 'bootstrap' hook

  return Array<Object>;
}
Enter fullscreen mode Exit fullscreen mode

data()

Prepares the data required in the Blog.svelte file. Whatever object is returned will be available as the data prop. In the example, we're returning a static string, but you could do anything you can do in Node such as:

  • Hit an external CMS
  • Query a database
  • Read from the file system
data: async ({

  data, // any data set by plugins/hooks on 'bootstrap' hook

  helpers, // helpers from ./src/helpers/index.js` file

  allRequests, // all `request` objects returned by all() function

  settings, // Elder.js settings

  request, // the requested page's `request` object

  errors, // any errors

  perf, // performance helper

  query, // search for 'query' in docs for more details

}): Object => {
  return Object;
};
Enter fullscreen mode Exit fullscreen mode

In this example, we're just returning a simple object in our data() function, but we could have easily used node-fetch and gotten our blogpost from a CMS or used fs to read from the filesystem:

const blogpost = await fetch(
  `https://api.mycms.com/getBySlug/${request.slug}/`
).then((res) => res.json());
Enter fullscreen mode Exit fullscreen mode

Why Routing Differs from Express-like Frameworks

While Elder.js' approach to routing is unconventional it offers several distinct advantages over traditional 'parameter based' routing:

Elder.js doesn't have to crawl all links of a site to know what pages need to be generated.

  • Build times can be fully parallelized and scale with CPU resources.
  • ElderGuide.com has ~20k pages and builds in 1 minute 22 seconds as of October 2020.

Users have full control over their URL structure.

This makes internationalization (i18n) and localization (l10n) more approachable. No complex regex is needed to have:

  • /senior-living/:facilityId/
  • /senior-living/:articleId/
  • /senior-living/:parentCompanyId/

Route.js Best Practices

A route's all function should return the minimum viable data points needed to generate a page; skinny request objects and fat data functions. It's possible to load the request objects returned by a route's all function with tons of data.

But this doesn't scale very well, so it's recommended you only include the bare minimum on the request object for querying your database, api, file system, or data store. Fetching, preparing, and processing data should then be done in the route's data function.

Real World Example

Imagine you're building a travel site listing tourist attractions for major cities. You have a city route and for each page on that route you need 3 data points to query for pulling in the rest of the page's data:

  1. Language of the page being generated
  2. City slug
  3. Country slug

Here is a minimal route.js for supporting /en/spain/barcelona/ and /es/espana/barcelona/.

// ./src/routes/city/route.js

module.exports = {
  permalink: ({ request, settings }) =>
    `/${request.lang}/${request.country.slug}/${request.slug}/`,

  all: async () => {
    return [
      {
        slug: "barcelona",
        country: { slug: "spain" },
        lang: "en"
      },
      {
        slug: "barcelona",
        country: { slug: "espana" },
        lang: "es"
      },
    ];
  },

  data: async ({ request }) => {
    // discussed below
  },
};
Enter fullscreen mode Exit fullscreen mode

Problems with Fat Request Objects

If we included in our request objects all of the additional details needed for generating the page it would look like this:

module.exports = {
  // permalink function

  all: async () => {
    return [
      {
        slug: 'barcelona',
        country: { slug: 'spain' },
        lang: 'en',
        data: {
          hotels: 12,
          attractions: 14,
          promotions: ['English promotion'],
          ...lotsOfData
        }
      },
      {
        slug: 'barcelona',
        country: { slug: 'espana' },
        lang: 'es',
        data: {
          hotels: 12,
          attractions: 14,
          promotions: ['Spanish promotion'],
          ...lotsOfData
        }
      }
    ]
  },

  // data function
}
Enter fullscreen mode Exit fullscreen mode

Imagine you're getting more data in your data function.

module.exports = {
  // permalink function

  // all function

  data:  async ({ request }) => {
    const hotels = [
        { ...hotel }, // lots of details
        { ...hotel },
        { ...hotel },
        { ...hotel },
        { ...hotel },
      ];

    return {
      hotels,
    };
  },
}
Enter fullscreen mode Exit fullscreen mode

hotels will now be available in your svelte template as your data param. You could access all of the hotel details at data.hotels. You have both request and data objects in Svelte templates.

But which of these do you need to access to get the number of hotels?

  • request.data.hotels
  • data.hotels.length

Only store the minimum data needed on your request objects. Return all of the data required by the page from the data function.

Database Connections, APIs, and External Data Sources

The data function of each route is designed to be the central place to fetch data for a route but the implementation details are open ended. If you are hitting a DB and want to manage your connection in a reusable fashion, it is recommended that you populate the query object on the bootstrap hook. This allows you to share a database connection across the entire lifecycle of your Elder.js site.

Cache Data Where Possible Within Route.js Files

If you have a data heavy calculation required to generate a page, look into calculating that data and caching it before your module.exports definition.

// ./src/routes/city/route.js

// heavy calculation here so data isn't calculated each request

const cityLookupObject = {
   barcelona: {
   // lots of data.
  }
}

module.exports = {
  permalink: ({ request, settings }) =>
    `/${request.lang}/${request.country.slug}/${request.slug}/`,

  all: async () => {
    return [
      {
        slug: "barcelona",
        country: { slug: "spain" },
        lang: "en"
      },
      { 
        slug: "barcelona",
        country: { slug: "espana" },
        lang: "es"
      },
    ];
  },

  data: async ({ request }) => {
    return {
      city: cityLookupObject[request.slug];
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

Data Used in Multiple Routes

If you have data in multiple routes, you can share that data between routes by populating the data object on the boostrap hook. For example, if you populated data.cities with an array of cities on the boostrap hook:

// ./src/routes/city/route.js

module.exports = {
  permalink: ({ request }) => `/${request.slug}/`,

  all: async ({ data }) => data.cities,

  data: async ({ request, data }) => {
    return {
      city: data.cities.find(
        city => city.slug === request.slug
      );
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

Discussion (0)

pic
Editor guide