21 minute read

Getting a Next.js application up and running is not a trivial exercise, especially if you want a robust and extensible result that will support a modern development process.

Here’s a plug-and-play Next.js template that offers the following features:

  • Tree-shakable support for the latest ES6 goodies with eslint uber alles.

  • User registration & authentication via NextAuth.js, by default against an AWS Cognito User Pool supporting native username/password authentication and one federated identity provider (Google).

  • Support for public & private API endpoints, both local to NextJS and at any AWS API Gateway secured by the same Cognito User Pool.

  • Configured to act as a front end & authentication client for my AWS API Template on the back end.

  • Fully integrated application state management with the Redux Toolkit, including support for difficult-to-serialize types like Date & BigInt.

  • Responsive UX with Semantic UI React with LESS theme overrides enabled & ready for input!

  • A responsive & attractive sample UI that encapsulates a ton of common use cases into an opinionated architecture and a library of utility components.

  • Automated lodash cherry-picking with babel-plugin-lodash.

  • Front & back-end testing with mocha, chai, and the React Testing Library. Includes examples and a sweet testing console!

  • Code formatting at every save & paste with prettier.

  • One-button release to GitHub with release-it.

See it on GitHub!   Clone the Repo!

This documentation is incomplete! You’ll see TODOs below where I’m still working on it, but since the underlying template is very solid I wanted to get it out there sooner rather than later. I’ll get it all done soon. Meanwhile don’t hesitate to raise an issue for any questions!

Why?

Deploying a vanilla Next.js application is easy, but getting it to a point where it can support real-world requirements is a challenge. This template solves a lot of initial problems and gets you to a well-scaffolded, responsive web application with support for all the goodies, built-in navigation, and a powerful toolbox to drive future development.

This template is highly opinionated with respect to toolchain. It is hard to get all of these bits to work together. It is way easier to cut out the bits you don’t need than figure out how to slot in the things you do. So that’s what we have here.

Because this is a Next.js template, it works perfectly when deployed to Vercel. I’ve tried hard to make it host-agnostic, though, and I know for a fact that it works just as well deployed to AWS Amplify. It should work fine (possibly with some tweaks to the build process) at any host that supports Next.js.

Setting Up Your Dev Environment

Use VS Code as your code editor! Not an absolute requirement, but you’ll be glad you did.

  1. Click here to generate a new repository from this template.

  2. Clone the new repository to your local machine.

  3. VS Code will ask to install a bunch of recommended extensions. Accept all of them. If you miss this prompt, follow these steps:

    • Open the VS Code Extensions tab
    • Enter @recommended into the search box
    • Click the Download link.

  4. Zero the package version and install dependencies by running these commands:

    npm version 0.0.0
    npm install
    
  5. Run your tests from the command line:

    npm run test
    
    # STATE
    #   ENTITY
    #     * validations
    #       ✔ * initializes state
    #     add entities
    #       * validations
    #         ✔ * select all entities
    #         ✔ * select one entity
    #         ✔ * select invalid entity
    #       update entities
    #         validations
    #           ✔ * select all entities
    #       remove entity
    #         validations
    #           ✔ * select all entities
    #
    # back-end test
    #   ✔ passes
    #
    # 7 passing (49ms)
    

    If you installed the VS Code extensions referenced above, use the Testing panel to visualize & run your unit tests.

  6. Run your Next.js application locally by running:

    npm run dev
    
  7. Explore the sample application at http://localhost:3000.

  8. When you’re done, return to your terminal and stop the dev server with Ctrl-C.

Create Local Environment Variable Files

Look for these files in your project directory:

Copy each of these files to the same location and remove the template extension from the copy.

Do not simply rename these files! Anybody who pulls your repo will need these templates to create the same files in his own local environment.

In the future, this will be accomplished with a single CLI command. (#45)

Connect to GitHub

This template supports automated release management with release-it.

If you use GitHub, create a Personal Access Token and add it as the value of GITHUB_TOKEN in .env.local.

If you use GitLab, follow these instructions and place your token in the same file.

For other release control systems, consult the release-it README.

You can now publish a release to GitHub with this command:

npm run release

Project Architecture

Basic project architecture is driven by the requirements of Next.js.

Next.js exposes a pages directory, which contains three categories of file that correspond to application routes.

  • _app.jsx defines the Application component described in Page Model below. It expresses the “frame” of the application, and all other visible components are injected into it.
  • The api subdirectory exposes a special category of routes that receive HTTP requests and run on the server side.
  • All other files within the pages directory translate to routes visible on the front end.

Paths inside the pages directory support a square bracket syntax that translates to route variables available at run time (e.g. /pages/api/customer/[customerId]/index.jsx).

A key feature is the state layer provided by the Redux Toolkit. Properly executed, this layer should handle most external requests and manage most state that needs to be passed between React components.

File Structure

Item                                 Description
---------------------------------------------------------------------------------------------------------
└─ nextjs-template ................. package root
   ├─ .babelrc ..................... global Babel config
   ├─ .env ......................... public global variables
   ├─ .env.local ................... locally-maintained private global variables
   ├─ .env.local.template .......... private global variable template
   ├─ .eslintignore ................ global ESLint ignore file
   ├─ .eslintrc.json ............... global ESLint config file
   ├─ .prettierignore .............. global Prettier ignore file
   ├─ .vscode ...................... VS Code project config
   │  ├─ extensions.json ........... recommended extensions
   │  └─ settings.json ............. project settings
   ├─ amplify.yml .................. AWS Amplify build script
   ├─ components ................... React components
   │  ├─ application ............... application-level components
   │  │  ├─ layout ................. application-level layout components
   │  │  │  ├─ PageFooter.jsx ...... common page footer
   │  │  │  └─ PageHeader.jsx ...... common page header
   │  │  ├─ session ................ application-level session management components
   │  │  │  ├─ SessionDropdown.jsx . renders session dropdown button
   │  │  │  ├─ SessionMenuItem.jsx . renders session menu item
   │  │  │  └─ useSignOut.jsx ...... encapsulates sign-out behavior
   │  │  ├─ sidebar ................ application-level sidebar components
   │  │  |  ├─ LinkMenuItem.jsx .... conditionally renders a menu item as an icon link
   │  │  |  ├─ PageMenuItem.jsx .... renders a state-sensitive menu item targeted at a page
   │  │  |  ├─ ScrollMenuItem.jsx .. renders a menu item targeted at a scroll location
   │  │  |  ├─ SidebarButton.jsx ... renders a button that triggers an overlay menu on mobile devices
   │  │  |  ├─ SidebarItems.jsx .... renders all sidebar items
   │  │  |  └─ SidebarLinks.jsx .... renders sidebar links repeated in page footer
   │  │  └─ util ................... application-level utility components
   │  │     ├─ RenderIf.jsx ........ conditionally renders a component based on current page & auth state
   │  │     └─ ScrollTarget.jsx .... HOC decorates a component with a smooth-scroll target
   │  ├─ content ................... content-level components (Page Component children)
   │  │  ├─ ApiQuery.jsx ........... tests a query (optionally authenticated) & displays result
   │  │  └─ ApiTest.jsx ............ displays a set of queries
   │  └─ page ...................... Page Components
   │     ├─ HomePage.jsx ........... displays ApiTest on a public page
   │     └─ PrivatePage.jsx ........ displays ApiTest on a private page
   ├─ env .......................... environment-specific environment variables
   │  ├─ .env.dev .................. public dev environment variables
   │  ├─ .env.dev.local ............ locally-maintained private dev environment variables
   │  ├─ .env.dev.local.template ... private dev environment variable template
   │  ├─ ... ....................... other environment variables
   ├─ middleware.js ................ NextAuth.js middleware config
   ├─ next-env.d.ts ................ Next.js required file
   ├─ next.config.mjs .............. Next.js configuration
   ├─ package-lock.json ............ package dependencies (managed by npm)
   ├─ package.json ................. package config
   ├─ pages ........................ Next.js page & api routes
   │  ├─ api ....................... Next.js api routes
   │  │  ├─ auth ................... NextAuth authentication route
   │  │     └─ [...nextauth].jsx ... NextAuth.js authentication endpoint
   │  │  ├─ hello.jsx .............. Sample public api endpoint
   │  │  └─ private ................ Sample private api route
   │  │     └─ hello.jsx ........... Sample private api endpoint
   │  ├─ coming-soon.jsx ........... coming soon route
   │  ├─ index.jsx ................. public home page route
   │  ├─ private.jsx ............... sample private page route
   │  └─ _app.jsx .................. Application component
   ├─ public ....................... public assets
   │  └─ images .................... public image assets
   │     ├─ favicon/ ............... public favicon assets
   │     └─ logo.png ............... site logo
   ├─ README.md .................... package README file
   ├─ semantic-ui .................. Semantic UI custom theme assets
   │  ├─ site/ ..................... Semantic UI custom theme variables & overrides
   │  └─ theme.config .............. Semantic UI custom theme config
   ├─ state ........................ Redux Toolkit state layer
   │  ├─ entitySlice.mjs ........... Sample Entity state
   │  ├─ pageSlice.mjs ............. Application page state
   │  └─ store.mjs ................. Redux Store definition
   ├─ styles.css ................... misc styles not defined in Semantic UI theme
   ├─ test ......................... unit tests
   │  ├─ entity.test.jsx ........... sample Entity state tests
   │  └─ sample.test.mjs ........... sample unit tests
   └─ tsconfig.json ................ required file

NPM Scripts

Script Description
npm run analyze Analyze browser & server bundles.
npm run analyze:browser Analyze browser bundle.
npm run analyze:server Analyze server bundle.
npm run build Generate a Next.js production build.
npm run dev Build & run the Next.js application locally.
npm run release Run unit tests & production build, then create a new release & publish to GitHub.
npm run start Build & run the Next.js production build locally.
npm run test Execute unit tests.

Environment Variables

Next.js has a tortured relationship with environment variables and dotenv files.

Modern software applications are configuration-driven. I ought to be able to deploy the same application into my various testing and production environments, each with a unique set of configurations.

In a rational world, there are four categories of dotenv file:

Scope Secret? Example
Application No .env
Application Yes .env.local
Environment No .env.test
Environment Yes .env.test.local

The first two are application-wide, but I should be able to create as many versions of the last two as I have environments, and load them appropriately on deployment.

Non-secret files generally get pushed to the code repository, whereas secret files are preserved locally and their contents encoded into each environment’s build pipeline.

The issues:

  • Next.js only supports three such environments, which must be named development, test, and production.

  • Next.js doesn’t load these files consistently across environments.

  • Next.js has complex rules around which variables are visible where (server side or in the browser).

  • There is no easy way to get Next.js to load different dotenv files from a different location.

See the Next.js docs for more info. Meanwhile, deployment environments also get a say.

Next.js can only load the files that are actually available to it. On your local development environment, everything will work as expected. Vercel (the native Next.js platform) and AWS Amplify (also an excellent choice) expose different sets of files to the Next.js build engine at different points in the process.

Finally, there is a way to load environment variables directly into Next.js, although doing so exposes ALL such variables to the browser, instead of just the ones with the NEXT_PUBLIC_ prefix.

So it’s a rich tapestry.

This template expresses an approach that offers the following features:

  • You can define as many environments as you want and name them however you like.

  • It works in your local dev environment and works well with both Vercel and AWS Amplify.

  • Public variables are consistently visible in the browser when prefixed with NEXT_PUBLIC_, and private variables are only visible on the server side.

By way of demonstration, this template’s live dev, test, and prod demo environments integrate perfectly with my AWS API Template’s corresponding demo environments.

There are three components to this approach:

  • dotenv files
  • Next.js config
  • Build config

dotenv Files

Application-scoped dotenv files live in the main project directory. There are three of them:

File Description
.env Application settings. Syncs with the code repo.
.env.local Application secrets. Does not sync with the repo.
.env.local.template Application secrets template. Syncs with the repo. Copy it the first time you pull the repo to create an application secrets file and populate it to support local testing.

Environment-scoped dotenv files live in the env directory. There is no limit to the number and names of supported environments. Each environment requires three files, e.g. for the test environment:

File Description
.env.test Environment settings. Syncs with the code repo.
.env.test.local Environment secrets. Does not sync with the repo.
.env.test.local.template Environment secrets template. Syncs with the repo. Copy it the first time you pull the repo to create an environment secrets file and populate it to support local testing.

Next.js Config

The application-scoped dotenv files have names and locations as expected by Next.js.

When running locally, they will be loaded. When deploying remotely as part of a build process, the application secrets will not be available and must be integrated with the build process as described below.

Environment-scoped dotenv files do NOT have names or locations as expected, and Next.js will NOT pick them up. Accordingly, I’ve added code to next.config.mjs that loads these files based on an environment token passed into the ENV variable. So to run Next.js locally using the test runtime environment, you would run:

cross-env ENV=test npm run dev

Note that this ALSO loads application secrets, thus exposing them to the browser! This is not a problem because these .local files are only available locally. Application & environment secrets are loaded differently in the remote build process, and since these files are not present their contents will NOT be exposed to the browser in remote deployments.

Build Config

As a general rule, all application and environment secrets must be encoded into any remote build process. Both Vercel and AWS Amplify support build-specific environment variables, so each must be configured accordingly.

For Vercel, this is sufficient.

For Amplify, there is an additional problem: the contents of the env directory are not even available to next.config.mjs. So I’ve included an amplify.yml build script that merges all available environment variables, secret and otherwise, into the one .env file that Amplify seems to understand.

dotenv Bottom Line

Follow these rules:

  • Use the four types of file as described in dotenv Files above.

  • If you want a variable to be available in the browser, prefix its name with NEXT_PUBLIC_.

  • Encode all application & environment secrets into your build process.

  • Additionally encode an ENV variable into your build process that carries your environment token (e.g. dev, test, or prod).

  • If you are deployng to AWS Amplify, and need to add more application or environment secrets, use the pattern in the amplify.yml preBuild section to integrate your new secrets with the build.

State Model

This template relies heavily on the Redux Toolkit for application state management.

Redux Toolkit is an efficient, highly opinionated wrapper around the very popular Redux library. It’s better in every way and is the approach recommended by the Redux team.

Getting Redux Toolkit to play nicely with Next.js is not a trivial exercise and requires making some choices. This template solves those problems in an elegant way.

Redux Toolkit features the createEntityAdapter (CRA), which generates a set of prebuilt reducers and selectors for performing CRUD operations on a normalized state structure containing instances of a particular type of data object. Think of it as a NoSQL database in your Redux state!

The sample front-end application doesn’t leverage CRA. Any significant data-based implementation probbly should, though, so I’ve included a sample entity slice in my state model and written some tests to demonstrate how it works.

The State Model comprises the Redux Store and its component slice definitions, all of which are located in the state directory.

Redux Store

The Redux Store is located at state/store.mjs.

The Store brings together slice reducers defined elsewhere in the state directory. It also adds middleware to the state layer, and applies the next-redux-wrapper that conforms the Next.js page model to Redux.

Notably the store adds the serify-deserify middleware, which resolves some difficulty around persisting complex types in your Redux Store.

This file will normally only require updates under these circumstances:

  • If you add a slice to your state layer, import the slice reducer here and add it to the master reducer definition.

  • If you wish to change your Redux middleware configuration or add new middleware, do it here.

Page Slice

The Page slice tracks the following broad application states:

State Description
baseUrl This is the application’s base url for the current environment. It is set here on the server side at initial load and should not need to be set again thereafter.
comingSoon This reflects the value of the COMING_SOON environment variable (defined in the corresponding env public file) and is used here on the server side to redirect the application to the coming-soon route if true. It is also consumed in sidebar components to display links as approprate.
currentPage This enumerated value is defined here and indicates the application’s current page. This value is set directly within a Page Component to identify the current page. To navigate to a different page Instead, use the resolveRoute helper function to get the correct route and pass this value to the pushRoute state.
logoutUrl This is derived here on the server side at initial load based on the environment’s AWS Cognito configuration and is passed to NextAuth.js as appropriate.
pushRoute A change to this state invokes this function in _app.jsx to push the new route, change page state as appropriate, and reset pushRoute.
sidebarVisible This controls the visibility of the overlay sidebar at mobile resolutions. It is set by the SidebarButton component and consumed in _app.jsx.
siteName This state composes the canonical site name with a token reflecting the environment. It is consumed in the SidebarItems component and wherever else appropriate.

currentPage reflects logical page state, and is intended to be decoupled from the actual route or page component in use. This permits sidebar and related components to reflect current page in a manner that is intuitive to the user.

To add a new value:

  • Add a new value to the PAGES enumeration.

  • Update the resolveRoute helper function to map the new value to the appropriate route.

resolveRoute currently maps routes based solely on a PAGES value. Feel free to add more logic to support more complex behaviors.

Entity Slice

The Entity slice does not participate in the sample application. It is included to illustrate the use of createEntityAdapter and related functionality.

See the source code and associated unit tests for more info.

User Authentication

User authentication is enabled using NextAuth.js against an AWS Cognito User Pool.

For simplicity, we are assuming that the Cognito User Pool has its hosted UI configured. This supports a wide variety of federated login providers, including social logins like Google, Facebook, etc. See my AWS API Template for an example of how to set this up.

NextAuth can secure front-end routes (i.e. pages) and back-end routes (i.e. API endpoints). You can also pass session credentials to other resources secured by the same authentication provider, for example an AWS API Gateway route. See the demo site for an example of this in action.

Semantic UI

This template uses the Semantic UI React component set.

Your starting point is a nice reactive layout with a sticky sidebar that collapses down to a hamburger menu at mobile resolutions. There were some difficulties getting this to work properly with the installed version of Semantic UI; I’ve resolved these and commented those fixes in the code.

The Semantic toolkit is super flexible, so you can easily morph this into whatever layout works for you.

Semantic UI has a fantastic LESS-based theming system. It was a HUGE challenge getting this working properly within the Next.js context. Problem solved, though, so out of the box this template offers full Semantic UI theme support.

All aspects of the site theme can be controlled by modifying the contents of the semantic-ui directory.

Out of the box, this template leverages the Semantic UI default theme. Switch themes globally or at a component level by modifying theme.config. Override every conceivable aspect of the current theme, with full access to all related LESS variables, by editing the templates in the site directory.

To examine existing themes and borrow their settings as overrides, follow the instructions to set up your dev environment and see the contents of directory node_modules/semantic-ui-less/themes.

Click here to learn more about Semantic UI themes.

Page Model

This template decouples route components, page components, and page contents. As a result:

  • Page components can be reused for similar use cases across multiple route components.

  • Page components can be conditionally displayed or composed with route components or other page components.

This diagram illustrates these relationships:

Application Component- Next.js root page component at  _app.jsx- Performs server-side redirections.- Completes server-side stateinitialization.- Invokes client-side stateinitialization.- Renders page frame.Route Component- Located within the pages directory.- Extracts route variables.- Manages route-specific state.- Selects & configures page component.Page Component- Top of in-frame display hierarchy.- Abstracts common page elementsacross routes.- Manages page-specific state.- Renders page content.Content Componentprop injection11configures1*configures1*configures1*

Application Component

The Next.js Application component is located at _app.jsx.

The Application component sets the “frame” of the page. It renders:

  • All global META and LINK tags.
  • The page header & footer.
  • The page sidebar or sidebar overlay, depending on page resolution.

This component also handles any changes to pushRoute state (see Page Slice above for more info).

Additionally, this component’s getInitialProps function handles any server-side processing required prior to initial page load, including:

  • Composing initial client-side state derived from server-side secrets or requiring access to the request object.

  • Performing server-side redirects requiring logic beyond simple route-mapping (e.g. to the Coming Soon page).

Route Components

Route components include all those located within the applications pages directory, besides _app.jsx.

These components can exploit Next.js dynamic routing conventions to extract parameters from the requested route.

In principle, route components could display any content. In practice, it is useful to abstract display to Page Components and allow Route Components to focus on:

  • Extracting route parameters and setting appropriate state.

  • Determining which Page Components to display and passing appropriate configuration to them.

Page Components

The Page Component is the top of the in-frame display hierarchy.

Every Page Component should exploit setCurrentpage.

Beyond this, every Page Component is resonsible for managing its own display and state as with any other React component.

Redirects

Out of the box, Next.js supports fairly robust server-side redirects. But there are always occasions when you need more… either more robust redirect logic on the server side, or a no-hassle mechanism to support redirects on the client side.

This template supports all three.

Global Redirects

Global redirects in this Next.js template are configured in next.config.mjs. Note that none of these are configured in this template by default.

Next.js global redirects support all kinds of pattern matching based on the contents the path, query string, request headers, and cookies. See the Next.js documentation for more info.

Back-End Redirects

There are scenarios where you need more than simple pattern-matching to perform a server-side redirect.

A good example in this template is the Coming Soon page, where redirects depend on the value of the COMING_SOON environment variable, whose value may differ from one anvironment to another.

Redirects in this case are performed in the Application component’s getInitialProps function. On initial load, this function has access to:

  • The full request object.
  • Public & private environment variables.
  • The initial Redux state.

Redirect here is performed using the redirect helper function. See the template’s getInitialProps function for more info.

Front-End Redirects

Redirects on the front end can be performed directly using next/router.

A special category is a redirect to any page defined in the PAGES enumeration and supported by the resolveRoute helper function.

In this case, use the setCurrentpage reducer to set the currentPage state as illustrated here.

Configured Pages

The template application has three pages configures:

Private Page

The page at /private is only visible to authenticated users. This is accomplished by adding the route pattern to the config variable in middleware.js, like this:

export const config = { matcher: ["/private"] };

If an unauthenticated user attempts to access this page, he will be redirected to a login page.

Note that the link to the Private page only appears in the sidebar when the user is authenticated. This is accomplished in SidebarItems.jsx. Note that router.push does not support shallow routing to protected pages!

Coming Soon Page

If the following environment variable condition is true, the application will display a coming soon page:

process.env.NEXT_PUBLIC_COMING_SOON === "1" &&
  process.env.NEXT_PUBLIC_VERCEL_ENV !== "preview";

If it is false, then the application will display.

In the development environment, both variables may be set explicitly in .env.development. In deployed environments, they are set explicitly in .env.production but may be overridden in your deployment pipeline.

If you are hosted at Vercel, the hosting environment will populate the NEXT_PUBLIC_VERCEL_ENV environment variable to reflect your deployment type. This value will be production on your production branch and preview on all other branches.

Common Tasks

Add a Page

TODO

Add an API Route

TODO

Create & Run a Local Production Build

TODO

Analyze Your Bundles

TODO

Create a Release

TODO

Integrate a Template Update

Follow these instructions.

FAQ

Why are all your tests .jsx files?

I know, right? And you’re gonna HATE the answer.

Issues

  • Authorization fails in local prod (i.e. npm run build && npm run start in your dev environment). This behavior arises from this NextAuth issue. I’m tracking it actively.

Leave a comment