A blog about React and related technologies

Exploring Next.js 9 Dynamic Routing & API Routes

Next.js 9 has been released yesterday with some amazing new features. Read the announcement post on the Next.js blog to learn about all new improvements.

In this post we'll explore how to use the new file system based dynamic routing and API routes. I created a simple RSS feed reader:

You can download the complete source code on GitHub.

Creating an API

Until Next.js 9, whenever we wanted to create an API for our website, we had to implement it separately from our Next.js app. There is nothing wrong with that (and in some cases it's preferred to implement the API separately), but for simple projects it's very convenient to be able to do it within a Next.js app.

Next.js 9 allows us to implement API routes via the /pages/api/ directory. All files inside that directory will be automatically mapped to /api/<route>. For example, /pages/api/posts.js (or /pages/api/posts/index.js) will be mapped to /api/posts.

For the example Feed Reader app, I created two API endpoints: http://localhost:3000/api/feeds to get the list of feeds (websites) and http://localhost:3000/api/feeds/<feed> to get the latest posts from a specific feed.

For /api/feeds I created a /pages/api/feeds/index.js file:

import feeds from '../../../data/feeds';

export default async (req, res) => {
  res.status(200).json(feeds);
};

It imports a list of feeds from a data file and serves it as json data:

[
  {
    "title": "Smashing Magazine",
    "slug": "smashingmagazine",
    "website": "https://www.smashingmagazine.com",
    "url": "https://www.smashingmagazine.com/feed/"
  },
  {
    "title": "CSS Tricks",
    "slug": "css-tricks",
    "website": "https://css-tricks.com",
    "url": "http://feeds.feedburner.com/CssTricks"
  },
  {
    "title": "freeCodeCamp",
    "slug": "freecodecamp",
    "website": "https://www.freecodecamp.org/news/",
    "url": "https://www.freecodecamp.org/news/rss/"
  },
 ...

To be able to have separate urls for each individual feed like http://localhost:3000/api/feeds/css-tricks, we need Dynamic Routing.

Every feed in our list has a unique slug which can be used for the query parameter.

I created a /pages/api/feeds/[slug].js file and Next.js will automatically pass the slug as the query parameter to the page. So, when someone requests the http://localhost:3000/api/feeds/css-tricks api route for example, the query object in the /pages/api/feeds/[slug].js file will be { slug: 'css-tricks' } and it can be used to generate data for that specific feed.

The file looks like this:

import fetch from 'isomorphic-unfetch';
import feeds from '../../../data/feeds';
import { distanceInWordsToNow } from 'date-fns';
import striptags from 'striptags';

export default async (req, res) => {
  const filtered = feeds.filter(item => item.slug === req.query.slug);

  if (filtered.length > 0) {
    let feed = filtered[0];
    feed.posts = [];

    let r = await fetch(`https://api.rss2json.com/v1/api.json?rss_url=${feed.url}`);
    let data = await r.json();

    if (data && data.items) {
      data.items.map(post => {
        feed.posts.push({
          title: post.title,
          published: distanceInWordsToNow(post.pubDate) + ' ago',
          link: post.link,
          author: striptags(post.author),
          preview: striptags(post.description).slice(0, 300)
        });
      });
    }

    res.status(200).json(feed);
  } else {
    res.status(404).json({ error: 'Feed not found.' });
  }
};

I used req.query.slug to filter the list of feeds to get all info for the requested feed, then I fetched the feed data via the rss2json.com service which automatically converts XML data from RSS feeds to JSON.

Finally, I map through post items to prepare the data and the API endpoint returns the requested feed with posts included, for example:

{
  "title": "CSS Tricks",
  "slug": "css-tricks",
  "website": "https://css-tricks.com",
  "url": "http://feeds.feedburner.com/CssTricks",
  "posts": [
    {
    "title": "The Many Ways to Include CSS in JavaScript Applications",
    "published": "about 18 hours ago",
    "link": "https://css-tricks.com/the-many-ways-to-include-css-in-javascript-applications/",
    "author": "Dominic Magnifico",
    "preview": "\nWelcome to an incredibly controversial topic in the land of front-end development! I’m sure that a majority of you reading this have encountered your fair share of #hotdrama surrounding how CSS should be handled within a JavaScript application.\nI want to preface this post with a disclaimer: There i"
    },
...

Displaying data

For the site frontend, I created two pages: /pages/index.js for the site homepage that will display a list of feeds and /pages/feed/[slug].js dynamic page to display data of individual feeds.

To display the list of feeds, I created a /components/feed-list.js file:

import Link from 'next/link';

const FeedList = ({ feeds }) => {
  return (
    <div className="feeds">
      {feeds.map(feed => {
        return (
          <div className="feed" key={feed.slug}>
            <Link href={`/feed/[slug]`} as={`/feed/${feed.slug}`}>
              <a>
                <img src={`/static/images/${feed.slug}.png`} />
                <div>{feed.title}</div>
              </a>
            </Link>
          </div>
        );
      })}
    </div>
  );
};

export default FeedList;

The Link Next.js component is used for route transitions to individual feeds. The href param specifies the path to the page inside the pages directory and as param includes the path that will be displayed in the browser.

In the /pages/feed/[slug].js file we get the slug from the query variable and we use it to make a request to our API inside getInitialProps:

import fetch from 'isomorphic-unfetch';
import Error from '../_error';
import Page from '../../components/Page';
import PostList from '../../components/post-list';

export default function Feed({ feed }) {
  if (feed.error) {
    return <Error statusCode={404} />;
  }

  return (
    <Page title={feed.title}>
      <div className="feed">
        <img className="image" src={`/static/images/${feed.slug}.png`} />
        <h1>{feed.title}</h1>
        <div className="links">
          <a href={feed.website} target="_blank">
            Website
          </a>{' '}
          |{' '}
          <a href={feed.url} target="_blank">
            RSS Feed
          </a>
        </div>
      </div>

      {feed.posts.length ? <PostList posts={feed.posts} /> : <div>Posts are not available at the moment</div>}
    </Page>
  );
}

Feed.getInitialProps = async ({ res, query }) => {
  let r = await fetch(`http://localhost:3000/api/feeds/${ctx.query.slug}`);
  let feed = await r.json();

  if (feed.error && res) {
    res.statusCode = 404;
  }

  return {
    feed
  };
};

Notice that I created a custom Error page.

Next.js by default displays a 404 error page when someone requests a non existing page, but for dynamic routes we need to do it ourselves.

When someone requests a non-existing feed, for example http://localhost:3000/feeds/xyz, the feed page makes a request to http://localhost:3000/api/feeds/xyz API route which returns a 404 response with an error message because a feed with xyz slug doesn't exist.

In getInitialProps, I've set res.statusCode to 404 when the API request returns an error and in the render method I return the error page using <Error statusCode={404} />.

The rest of the Feed Reader app doesn't use any new features. One thing to mention is that since our homepage doesn't include getInitialProps, Next.js will automatically serve it as a statically generated page due to the new Automatic Static Optimization!