Skip to content
/ nextjs-boilerplate Public template

Boilerplate for projects based on NextJs + TypeScript

Notifications You must be signed in to change notification settings

danikaze/nextjs-boilerplate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

51 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nextjs-boilerplate

A boilerplate to use in projects with NextJs and TypeScript.

Build Status

Features

Ready

Planned

  • Runtime server settings read from filesystem
  • Migrate to typescript-eslint
  • Component testing
  • Visual regression testing
  • Typed CSS Modules

Setup

Points 1 and 2 can be combined if using this repository as a template when creating a new one.

  1. Clone this repository
git clone https://github.com/danikaze/nextron-boilerplate.git PROJECT_FOLDER
  1. Change the origin to the new repository
cd PROJECT_FOLDER
git remote rm origin
git remote add origin YOUR_REMOTE_REPOSITORY.git
git push -u origin master
  1. Change the name, description and version if needed in [package.json].

  2. Install the needed packages

npm install

Configuration

TypeScript path aliases

  • Edit the main/tsconfig.json file with the path aliases to be available.
  • Add them also to the no-implicit-dependencies rule in the tslint.yaml file.

NextJS based code will work without problem since it's webpack based.

Custom server code is compiled directly with tsc (no webpack support), so it runs applying tsconfig-paths to ts-node in development mode. For built code, a custom script replaces the path aliases with the correct routes at build time.

Development notes

Debugging

While running npm run dev, just hit F5 in vscode and it will attach automatically to the nextjs server process, making breakpoints available.

Alternatively you can also debug in chrome just going to chrome://inspect

Redux

Using Redux is optional in this boilerplate. It's enabled by default because the example is using it, but you can disable it easily by setting REDUX_ENABLED constant to false in global.js. Make sure you don't include anything from the store folder if you disable it, since there is where all the Redux-related code is contained.

The index.ts file basically exports the wrapper function used by next-redux-wrapper, so there's no need to modify it.

What it is important, are the three subfolders, which are described in the following subsections of this document:

Actions

Actions in the app use the Flux Standard Action convention which basically follows the defined AppAction interface:

export interface AppAction<T extends string, P = never, M = never> {
  type: T;
  payload?: P;
  meta?: M;
  error?: boolean;
}

Basically because it's the format required by the redux-promise-middleware, which expects the data to be inside the action.payload field.

The actions/index.ts describe this interface and also has an internal type called AppActionList which is the one which should be modified, adding the extra actions available in the real app. This allows to properly type the list of actions you can create and use in the reducer.

In the end, the real action list exported is just this list plus the HydrateAction, used again by next-redux-wrapper.

The idea is to have actions of one context/component grouped in one file inside the folder, like the ones provided as an example in counter.ts. This file provides basically three things:

  • A grouped type of all provided actions (CounterAction).
  • Interfaces for each action (IncreaseCounterAction and DecreaseCounterAction).
  • Action creators (increaseCount and decreaseCount).

Model

The model is basically the type definition of the Redux Store State. model/index.ts provides the State interface, which should be modified with the definition of your app state. Usually composed by other interfaces as shown in the example.

Each of this interfaces are initially grouped in the code example in folders, one for each context/component offering two files:

  • module/index.ts, which provides the interface for that part of the state.
  • module/selectors, that groups the different selectors to access data of that interface.

Remember that it's recommended to have multiple selectors accessing small pieces of data instead of having only one returning a big object.

Reducers

Reducers here works with the standard combineReducers from redux.

The only thing to do in reducers/index.ts which provides the global reducer file, is to fill the combinedReducer with the list of your context/container reducers.

The resulting reducer will be combined with the special hydrateReducer which is required by next-redux-wrapper.

CSS Modules/sass/scss

Next.js supports this styles with static optimization just setting up a few things. It's already done here. For more sass customization, you can edit the next.config.js where it says sassOptions.

Material-UI

Material-UI is supported including server side rendering as recommended in the library documentation. It generates style sheets in server side which are removed later in client side on the first render of the page.

The theme used by the app can be customized editing the files in @themes.

The package clsx is available by default if the preferred option is makeStyles but using withStyles is also an alternative to avoid dealing with classnames.

Because tree shaking is enabled via Babel, it is safe to import all the components in one line from @material-ui/core like the following code shows:

// ✔️ Without tree-shaking this would be needed
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';

// ✔️ Because tree-shaking is enabled, this is safe and still fast!
import { Button, TextField } from '@material-ui/core';

i18n

Translations work with next-i18next as is the de-facto standard for Next JS i18n.

Code splitting in localized data is enabled by default, meaning that only the needed translations will be provided when the page is rendered, and other required ones will be loaded when needed dynamically.

For this, localizations are split in namespaces (manually, depending on your application):

  • To enable i18n, (next-i18next.config.js)[./next-i18next.config.js] is required.
  • All localization files are in public/static/locales/LANG/NAMESPACE.json.
  • AVAILABLE_LOCALES will be a build-time constant automatically generated from the configuration in (next-i18next.config.js)[./next-i18next.config.js].
  • AvailableLocale will be a type automatically generated matching the values for AVAILABLE_LOCALES.
  • Required localization namespaces are needed for each page. Specify them by using getStaticProps or getServerSideProps (as done here).

Caveats

Because the new data fetching method (from NextJS 9) getServerSideProps is not fully supported, if a page requires dynamic initial props, there's the need to apply a workaround, which disables SSG (Static Site Generation) making every page to work with SSR (Server Side Rendering).

This workaround is optional (to be applied in build time or not), and can be enabled or disabled in [global.js] changing the value of I18N_OPTIMIZED_NAMESPACES_ENABLED.

If set to true:

  • i18n will send only the required namespaces to each page, keeping the initial render faster
  • SSG will be disabled (meaning every page will be SSR)

If set to false:

  • i18n will send all namespaces to each page
  • SSG will be enabled

By default this workaround is enabled, but it might be a good idea to disable it if:

  • Your localized data is small, or there's not much difference in loading all namespaces.
  • There's not much dynamic data and it's worth to have SSG instead of SSR.

Logging

This setup provides isomorphic logging, meaning that the same code is available in server and client side for simplicity.

When a logging call is executed in server side, it will be outputted to the logs folder (or any configured transport). If the same line is executed in client side, it will be outputted in the browser console. That is, depending on the log call level and the provided options.

Since different logging libraries provide different logging levels, and somewhat it's confusing on how to use them, this setup takes an opinionated approach and defines it here (however, feel free to use them in the way it better fits your needs):

method priority usage
error 0 errors affecting the operation/service
warn 1 recuperable error or unexpected things
info 2 processes taking place (start, stop, etc.) to follow the app workflow
verbose 3 detailed info, not important
debug 4 debug messages

By default, each page will receive a field logger in their props, initialized with a namespace like FoobarPage if the page is called Foobar.

You can define get extra namespaces just calling the hook useLogger('namespace') from your components anytime, defined in ./utils/logger.

Usually only one global logger would be used in an app, and its options can be customized by editing logger.config.js (interface and default values are specified here), but if required, you can wrap other parts of your code with Logger component (a React.Provider) since the useLogger hook will access the deeper one, meaning you can do things like this:

const Page: AppPage({ logger }) => {
  logger.info('Page rendered');

  return (
    <Logger value={new IsomorphicLogger(customLoggerConfiguration)}>
      <Component />
    </Logger>
  )
}

const Component = () => {
  const logger = useLogger('Component');
  logger.info('This will be logged with the custom logger');

  return (
    <div>Component contents</div>
  );
}

Because we want to use the logger outside the react components as well, not always can it be retrieved as a hook. For that, there's also the getLogger(namespace) function, which will use always the global logger configuration but it's accessible everywhere.

const logger = getLogger('API');
logger.debug('This debug line is for code outside react');

Authentication

Most web-apps require some kind of user authentication, and this boilerplate provides everything you need to set it up, based on passport.

Configuration

To enable authentication, just make sure AUTH_ENABLED is true in the build-time-constants.

There are other values that can be customized:

Constant Notes
AUTH_LOGIN_SUCCESS_PAGE The URL where the user is redirected after a successfully login attempt
AUTH_LOGIN_FAIL_PAGE The URL where the user is redirected after a failed login attempt
AUTH_LOGOUT_PAGE The URL where the user is notified that their credentials are cleared (logout is provided by default, but can be changed and/or customized)
AUTH_DO_LOGOUT_URL This is the page that will clear all user credentials and redirect to AUTH_LOGOUT_PAGE (only the URL needs to be defined, the page itself is already provided)
AUTH_LOGIN_REDIRECT_PARAM Parameter used (if defined) to provide the original URL for a redirection on a login success (currently only working for the local strategy)
AUTH_FORBIDDEN_PAGE When a logged-in user doesn't have enough permissions to access a page, it's redirected here (if set). If this is not set, a HTTP 401 Unauthorized error is sent. (forbidden is provided by default, but can be changed and/or customized)
Local Strategy

Local strategy is nothing more than applying a custom way of checking the user status, usually through a database, retrieving the user data and checking if the provided password checks at the login time. Then, in each request if the user exists, we just check its permission level.

In this example, the User model is identified by its username and an id field. It has a password, stored using a salt value for better security via scrypt.

Because this boilerplate is agnostic on the used database, it's using mock-data defined in the strategy configuration file, and the point 1 where the user data is retrieved, should be replaced with the proper implementation.

When checking the username and password, the strategy relies on those values coming from a form with that field names: username and password, as shown in the Login form.

Customizable constants are:

Constant Notes
AUTH_LOCAL_DO_LOGIN_URL This is the page that will receive the username and password parameters via POST, and redirects to AUTH_SUCCESS_PAGE or AUTH_FAIL_PAGE (only the URL needs to be defined, the page itself is already provided.
Twitter Strategy

This is an example of using an external service to authenticate your users. This especifically uses passport-twitter for it, and requires to set some constants as well:

In global.d.ts:

Constant Notes
AUTH_TWITTER_LOGIN_PAGE Local route that will redirect to the twitter one when initializing the auth process

In server.d.ts:

Constant Notes
AUTH_TWITTER_CALLBACK_ABS_URL Route that will process the result when authenticated via twitter
AUTH_TWITTER_API_KEY Your App API Key
AUTH_TWITTER_API_KEY_SECRET Your App API Key Secret

Make sure to place the values for AUTH_TWITTER_API_KEY and AUTH_TWITTER_API_KEY_SECRET in the server-secret.js file instead of server.js so they won't be commited to the repository.

Other Strategies

Because passport is ready to be used, other authentication strategies such as Github, Facebook or Google among others, can be easily integrated as well just adding them to the express server the same way it's done for Twitter.

Usage

Pages you want to protect require getServerSideProps. This will disable your SSG but it's something logic to happen if you want the rendering to depend on the actual permissions of the current user. Define this function by using userRequiredServerSideProps or adminRequiredServerSideProps from @utils/auth.ts.

The request object provided by the context object received in the getServerSideProps function will have a user property set to false if the user is not logged in, or the set object in the configured previously. The user data can be accessed with the hook useUserData from @utils/auth.ts.

The boilerplate example comes with a defined User model containing several data, but only information related to { id, username, role } is provided in the authentication cookie -encrypted- (it doesn't do a call to the model to retrieve that information in that request, but just read the encoded cookie), which is the minimum required to make it work. If more information is required, you can retrieve it from your model.

If based on the values the user should not have access to the page, the request can be redirected to other URL.

NOTE: A different approach can also be chosen without using getServerSideProps if it's OK to show the (empty) page to a user without credentials if the data is actually secure (fetched with a protected API).

Testing

Unit testing uses Jest as a test runner. It also provides assertion and mock functions, but Sinon is also available.

Executing npm run test will run all the tests and the linter, while npm run test-debug will keep jest running in watch mode and code can be inspected attaching the debugger to the process (F5 in Visual Code, or browsing to chrome://inspect, etc.).

To run only one test, it can be passed as a parameter (or some by usign globs). Just remember that you need to append -- to pass them when running npm run

npm run test-only -- utils/__test/auth.spec.ts

Every file named as .spec.ts, .spec.tsx, .test.ts or .test.tsx will be considered as test cases and loaded when running the tests, and by convention they are usually placed inside a __test folder where the feature is located.

When running the tests, a .coverage folder will be created (but not included in the repository) using Istanbul, which you can use to check which part of your code is missing testing by browsing the html reports.

Because tests add a IS_TEST global build-time constant, it's used to disable logging and prevent polluting the output when running the tests.

Note that because this repository already provides a travis-ci configuration, you only need to enable your repository in your account to start checking your code sanity with each commit without further work (travis executes npm run test by default for NodeJS projects).

Static file imports

Since NextJS 11, url-loader and file-loader are not used anymore. Instead, the Image component is recommended.

Bundle Analyzer

Just add ANALYZE=true in the environment variables to generate the report:

ANALYZE=true npm run build