Eliminating server.js with Next.js 9.5

Eliminating server.js with Next.js 9.5

Next.js 9.5 came out a week after releasing the 1.0 version of the Github Repository Template I have been building in this series. Next 9.5 includes a lot of great enhancements, but a key new feature was the rewrites config. With this feature, I hope to eliminate the server.js all-together. This will have a handful of promised benefits including being able to deploy to Vercel. In this post, I will be upgrading my Github Template Repo project and eliminating /server.js in the process.

Just want the code? Check out this diff of changes covered in this post since the 1.0 release. You can checkout the full project here.

Rewrite for static files

Some browsers and crawlers hit the legacy /favicon.ico url instead of obeying the rel="icon" meta tag src. This can clog up the logs with 404s which is unneeded noise. I usually add a simple rewrite in the server.js to deal with this. However, this can be eliminated now using Next.js rewrites.

First, I want to ensure the /favicon.ico route works. Here's what shows in the browser as served up by server.js

Screen Shot 2020-08-01 at 7.29.15 PM.png

Next I will eliminate this handler from server.js

// Legacy Favicon Url
server.get('/favicon.ico', (_req, res) => res.status(200).sendFile('favicon.ico', { root: `${__dirname}/public/static/` }));

Restarting the dev server via npm run dev and opening /favicon.ico in the browser, I see the route no longer works. Screen Shot 2020-08-01 at 7.25.11 PM.png

Next I can add this directive to the next.config.js

...
  async rewrites() {
   return [
      ...
      {
        source: '/favicon.ico',
        destination: '/static/favicon.ico',
      },
     ...
    ];
  },

This is the rewrites directive introduced in Next 9.5.

If I restart the dev server again and reload in the browser, I see my favicon.ico from the public/static/ directory loaded at the /favicon.ico route.

Screen Shot 2020-08-01 at 7.29.15 PM.png

As a final check, I will see what the headers look like on this path.

curl -v http://localhost:8080/favicon.ico 1> /dev/null

This shows I get a 200 return status (rather than a 302, etc) and the content-type and bytes match that of the actual route (/static/favicon.ico). Awesome!

curl -vs http://localhost:8080/favicon.ico 1> /dev/null
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /favicon.ico HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Accept-Ranges: bytes
< Cache-Control: public, max-age=0
< Last-Modified: Sat, 01 Aug 2020 14:39:27 GMT
< ETag: W/"3c2e-173aa769f33"
< Content-Type: image/x-icon
< Content-Length: 15406
< Vary: Accept-Encoding
< Date: Sat, 01 Aug 2020 17:54:16 GMT
< Connection: keep-alive
< 
{ [7808 bytes data]
* Connection #0 to host localhost left intact

This worked great! I now know I can leverage Next.js rewrites to serve up static files!

For kicks, I'm going to add a /robots.txt file as well. For brevity, I will not write about it, but you can see the final route rule here and the static file in this commit.

Rewrite for next-offline Service Worker

Another reason I was stuck with server.js was to serve up the service-worker.js that is auto generated by next-offline to provide PWA support. Due to the way Service Workers work, they have to be served from the url root to apply to pages in the root and below. This previously required an external routing solution, but I can leverage the new rewrites feature to accomplish this.

First, I'll remove this handler from server.js

server.get('/service-worker.js', (_req, res) => {
  res.status(200).sendFile('/service-worker.js', { root: `${__dirname}/build/` });
});

At this point there is little left in the server file.

Before I get too far, I should mention that, in addition to the /public/static directory leveraged above, certain files from the Next.js distDir (/.next/ by default) are served statically. If you inspect the network tab when loading the app, you can see bundles served from /_next/static. However, not all files in /_next/ are served up (eg. server assets, etc). I don't fully understand the relationship between the /_next/ url path and the build folder, but Tim from the Next.js team confirmed my inkling that only /_next/static is publicly available. Also, rewrites in general can only have destinations of publicly available urls.

With that knowledge, then the trick for service worker is to somehow move it to the /.next/static folder so that it is public and then make a rewrite rule with a destination of _next/static. Slightly confusing, but easy to accomplish.

First the rewrite:

  async rewrites() {
    return [
        ...
       {
        source: '/service-worker.js',
        destination: '/_next/static/service-worker.js',
       }
        ...
    ];
  },

Finally, I need to tell next-offline to write the service-worker.js file to the /.next/static/ dir instead of /.next/ dir (default behavior). I'll add this bit of undocumented magic to next.config.js

  workboxOpts: {
    swDest: './static/service-worker.js',
  },

After a production build and run, my service worker is happy.

Screen Shot 2020-08-01 at 7.51.45 PM.png

Leveraging PORT and NODE_ENV environment variables.

I usually deploy to Google App Engine (although I do want to give Vercel a whirl here soon). That said, GAE requires you to obey their PORT environment variable. The only remaining job of /server.js is to start the server obeying the PORT environment variable.

First, I need to update my dev script in package.json to not run the server.js

"dev": "node ./server.js",

replaced with:

"dev": "next -p ${PORT:-3000}",

Running npm run dev runs the same as before. Additionally, I can now define the port in the command line too:

PORT=4000 npm run dev

and the dev server starts on port 4000.

Finally, I'll apply the same concept to the start-local and start script:

"start": "next start -p $PORT",
"start-local": "NODE_ENV=production next start -p ${PORT:-8080}",

Now both obey the $PORT environment variable. Note: Since GAE runs the start command and the env vars are already present, I chose to leave them off.

That's it! Now I can completely remove my server.js and things feel slightly faster.

Note: Using environment variables like this in npm scripts may not work on certain Windows shells. I never do Windows development so I won't add it to my boiler plate, but if you do, check out the cross-env package.

Bonus: File Based Routing Dynamic Segment Conflict Resolution

An issue that affected my personal blog was the resolution of legacy urls. Consider the following four urls:

  • / The index of the site
  • /about A static page about me
  • /art A blog category page
  • /2020/02/12/magic-numbers-feigenbaum-constant A blog post page

The later 2 routes were established waaaay back in my Wordpress days. I get a fair amount of traffic to those routes. I also have incoming links, disqus references, etc that rely on those routes to exist. Changing them would be a pain.

With the introduction of Next.js 9's support for dynamic route segments, I thought I could get rid of the server.js resolution. However, there was a conflict in routes.

Consider the directory structure for the last 2 routes using dynamic route segments with path based routing: /[:category].js /[:year]/[:month]/[:day]/[:slug].js The first part of the route conflicts. Any url prefixed with 2020 was matching the [:category].js file. I attempted to rename the [:year] directory to 20[:year] but Next doesn't support that syntax.

I conceded early on and left my folder structure like:

/blog/category.js
/blog/article.js

and then used server.js to route urls based on regexp to either of those files.

However, with Next.js 9.5. rewrites, I revisited the issue and came up with the following pair of rewrite directives:

{ source: '/:year/:month/:day/:slug', destination: '/blog/article' },
{ source: '/:slug', destination: '/blog/category' },

This worked amazingly well and I didn't need to change any of my existing links. After debugging, the route params were passed as expected as well.

My guess is that pure file based routing does not deal with specificity whereas the rewrites directive does.

Discussion Topic

  • Do you have other uses for server.js not covered here that you're stuck with?
  • Have you utilized other aspects of the Next 9.5 release that made your life easier?

Image Credit: Pexels