Next.js 13 + App Router + MongoDB - User Rego and Login Tutorial with Example
Tutorial built with Next.js 13.4.12 with the App Router, React 18.2.0, TypeScript and MongoDB
In this tutorial we'll go through an example of how to build a simple user registration, login and user management (CRUD) application with Next.js and MongoDB.
The app is built with TypeScript and the new App Router that was introduced in Next.js 13.
Tutorial contents
- Example app overview
- Tools required to run the Next.js + MongoDB app
- Run the Next.js example app locally
- Next.js + MongoDB code documentation
- Other versions of this tutorial
Example Next.js + MongoDB Auth App Overview
The example is a full-stack Next.js application written in TypeScript that includes a React front-end and Next.js back-end API.
Next.js Tutorial Front-End App
The Next.js client app is built with React and contains the following pages:
/account/login
- public page for logging into the Next.js app./account/register
- public page for registering a new user account with the app./
- secure home page containing a simple welcome message to the logged in user./users
- secure page displaying a list of all users in the Next.js app, with options to add, edit or delete users./users/add
- secure page for adding a new user./users/edit/[id]
- secure page for editing an existing user.
Secure pages are protected by the Secure Layout Component which redirects unauthenticated users to the login page.
Next.js Tutorial Back-End API
The Next.js API contains the following routes/endpoints:
/api/account/login
- POST - public route for authenticating username and password and generating a JWT token that is returned in an HTTP onlyauthorization
cookie./api/account/logout
- POST - public route for logging out that deletes the HTTP onlyauthorization
cookie./api/account/register
- POST - public route for registering a new user with the Next.js app./api/users
- GET - secure route that returns all users./api/users
- POST - secure route for creating a new user./api/users/current
- GET - secure route that returns the current logged in user./api/users/[id]
- GET - secure route that returns the user with the specifiedid
./api/users/[id]
- PUT - secure route for updating a user./api/users/[id]
- DELETE - secure route for deleting a user.
Secure routes are protected by the JWT Middleware that verifies the JWT token in the authorization
cookie of the request.
Next.js App Router
The new App Router is similar to the existing Pages Router in that they are both file-system based routers where folders and files are used to define page and API routes in Next.js applications.
A different file naming convention is used to define routes with the Next.js Pages Router vs the App Router. For example if you want to create a login form with the route /account/login
, using the Pages Router you would create the file /pages/account/login.jsx
, whereas with the App Router you would create the file /app/account/login/page.jsx
(or .tsx with TypeScript).
The App Router adds support for new features including React Server Components, Layouts, Route Groups and Private Folders.
- React Server Components are rendered on the server, I think of them working similar to traditional server-side rendered web pages (e.g. .NET MVC, PHP etc).
- Layouts allow you to put common layout code in one place for multiple pages, this wasn't supported with the Pages Router and required workarounds.
- Route Groups allow you to group pages in folders without the folder name being included in the URL path. To do this wrap the folder name in parentheses (e.g.
(secure)
,(public)
). - Private Folders allow you to create folders in the
/app
directory that are ignored by the routing system. To do this prefix the folder name with an underscore (e.g._components
,_services
).
To use the App Router place files in the /app
folder, to use the Pages Router place files in the /pages
folder. It's possible to use both in the same project but this should only be required if you're migrating a project from the Pages Router to the App Router.
MongoDB and Mongoose ODM
MongoDB is the database used by the API for storing user data, and the Mongoose ODM (Object Data Modeling) library is used to interact with MongoDB, including defining the schemas for collections, connecting to the database and performing all CRUD operations. For more info on Mongoose see https://mongoosejs.com/.
React Hook Form Library
Forms in the example are built with React Hook Form - a library for building, validating and handling forms in React using React Hooks. I've been using it for my React and Next.js projects for a while now, I think it's easier to use than the other options available and requires less code. For more info see https://react-hook-form.com.
Zustand State Management
Zustand is a popular light-weight React state management library used by the user service and alert service to manage shared state that can be accessed by different components in the application. For more info see https://docs.pmnd.rs/zustand/getting-started/introduction.
Joi Schema Validation
Joi is a flexible JavaScript schema validation library used to validate data sent in HTTP requests to the Next.js API (e.g. the login route and register route). For more info see Request Schema Validation with Joi or https://joi.dev/.
Bootstrap CSS
Bootstrap 5 CSS is used for styling the Next.js tutorial app, it is imported in the Root Layout component. For more info see https://getbootstrap.com/.
Code on GitHub
The example project is available on GitHub at https://github.com/cornflourblue/next-js-13-app-router-mongodb-rego-login-example.
Tools required for this tutorial
To develop and run Next.js + MongoDB applications locally you'll need the following:
- Node.js and npm - includes the Node runtime and command line tools.
- Visual Studio Code - code editor that runs on Windows, Mac and Linux. If you have a different preferred code editor that's fine too.
- MongoDB - you'll need access to a running MongoDB server instance for the Next.js app to connect to, it can be remote (e.g. Azure, AWS etc) or on your local machine. The MongoDB Community Server is free and can be downloaded at https://www.mongodb.com/try/download/community. You can also run it in a Docker container, the official docker images for MongoDB are available at https://hub.docker.com/_/mongo.
- Ensure MongoDB is started so the app can connect to it - instructions are available on the install page for each OS at https://docs.mongodb.com/manual/administration/install-community/.
Run the Next.js + MongoDB Login Example Locally
Follow these steps to download and run the Next.js 13 + App Router + MongoDB Auth App on your local machine:
- Download or clone the Next.js project source code from https://github.com/cornflourblue/next-js-13-app-router-mongodb-rego-login-example
- Install all required npm packages by running
npm install
ornpm i
from the command line in the project root folder (where the package.json is located). - Start the app by running
npm run dev
from the command line in the project root folder, this will compile the app and start the Next.js server. - Open to the app at the URL
http://localhost:3000
.
NOTE: You can also start the app directly with the Next.js CLI command npx next dev
. For more info on the Next.js CLI see https://nextjs.org/docs/app/api-reference/next-cli.
Before running in production
Before running the app in production make sure you update the JWT_SECRET
environment variable in the .env file, it is used to sign and verify JWT tokens for authentication, change it to a random string to ensure nobody else can generate a JWT with the same secret and gain unauthorized access to your Next.js app or API. A quick and easy way is join a couple of GUIDs together to make a long random string (e.g. from https://www.guidgenerator.com/).
Next.js + MongoDB Code Documentation
Inside the top-level Next.js App Router folder (/app
), the project is organised into the following folders:
- _components
React components used by pages or by other React components. Global components are in the root/_components
folder and feature specific components are in subfolders (e.g./_components/users
). The folder name is prefixed with an underscore (_
) to mark it as a Private Folder which is ignored by the Next.js routing system. - _helpers
Anything that doesn't fit into the other folders and doesn't justify having its own folder. Front-end React helpers are in the/_helpers/client
folder, server-side helpers are in the/_helpers/server
folder and API specific helpers are in the/_helpers/server/api
folder. The folder name is prefixed with an underscore (_
) to mark it as a Private Folder which is ignored by the Next.js routing system. - _services
Services encapsulate client-side logic and handle HTTP communication between the React front-end app to the Next.js back-end API. Each service encapsulates logic for a specific content type (e.g. the user service) and exposes properties and methods for performing various operations (e.g. auth and CRUD operations). Services can also perform actions that don't involve HTTP requests, such as displaying and clearing alerts with the alert service. The folder name is prefixed with an underscore (_
) to mark it as a Private Folder which is ignored by the Next.js routing system. - (public)
Public pages including the login page and register page. The folder name is wrapped in parentheses()
to mark it as a Route Group so it isn't included in the URL path. - (secure)
Secure pages that are only accessible to authenticated users. Unauthenticated users are automatically redirected to the login page by the Secure Layout. The folder name is wrapped in parentheses()
to mark it as a Route Group so it isn't included in the URL path. - api
API route handlers for the Next.js login tutorial app. Route handlers are all wrapped with the custom API Handler function to add support for middleware and global error handling.
NOTE: I tried this with the new App Router Middleware but it doesn't seem to support error handling by wrappingNextResponse.next()
in atry/catch
block (like express middleware).
TypeScript code structure
I've organised TypeScript files with export
statements at the top so it's easy to see all exported modules when you open a file. Export statements are followed by functions and other implementation code for each TS module.
Barrel files
The index.ts
files in some folders (e.g. _components, _helpers, _services) are barrel files that re-export all of the modules from that folder so they can be imported using only the folder path instead of the full path to each module, and to enable importing multiple modules in a single import (e.g. import { Alert, Nav } from '_components';
).
Base URL for imports
The baseUrl
is set to "./app"
in the tsconfig.json file to make all import statements (without a dot '.' prefix) relative to the /app
folder of the project, removing the need for long relative paths like import { useUserService } from '../../../_services';
.
Next.js Project structure
Click any of the below links to jump down to a description of each file along with it's code:
- app
- _components
- users
- AddEdit.tsx
- index.ts
- Alert.tsx
- Nav.tsx
- NavLink.tsx
- Spinner.tsx
- index.ts
- users
- _helpers
- client
- useFetch.ts
- index.ts
- server
- api
- auth.ts
- db.ts
- users-repo.ts
- index.ts
- client
- _services
- useAlertService.ts
- useUserService.ts
- index.ts
- (public)
- account
- login
- register
- layout.tsx
- account
- (secure)
- users
- layout.tsx
- page.tsx
- api
- globals.css
- layout.tsx
- _components
- .env
- .eslintrc.json
- next.config.js
- package.json
- tsconfig.json
Users Add/Edit Component
The users AddEdit
component is used for both adding and editing users, it contains a form built with the React Hook Form library and is used by the add user page and edit user page.
The useForm()
hook function returns an object with methods for working with a form including registering inputs, handling form submit, resetting the form, accessing form state, displaying errors and more, for a complete list see https://react-hook-form.com/docs/useform.
Form validation rules are defined with options passed to the React Hook Form register()
function that is used to register each form input field. For a full list of validation options available see https://react-hook-form.com/docs/useform/register#options.
The onSubmit
function is executed when the form is submitted and valid, and either creates or updates a user depending on which mode the form is in.
The form is in edit mode when there is a user
object passed in the component props, otherwise it is in add mode. In edit mode the user details are preloaded into the form fields with the React Hook Form defaultValues
option. The password field is required in add mode and optional in edit mode, this is controlled with the custom validate
function of the password field.
The returned TSX/JSX template contains the form with all of the input fields and validation messages. The form fields are bound to each input element with the JS spread operater (e.g. {...fields.firstName}
).
'use client';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useForm } from 'react-hook-form';
import { useAlertService, useUserService } from '_services';
export { AddEdit };
function AddEdit({ title, user }: { title: string, user?: any }) {
const router = useRouter();
const alertService = useAlertService();
const userService = useUserService();
// get functions to build form with useForm() hook
const { register, handleSubmit, reset, formState } = useForm({ defaultValues: user });
const { errors } = formState;
const fields = {
firstName: register('firstName', { required: 'First Name is required' }),
lastName: register('lastName', { required: 'Last Name is required' }),
username: register('username', { required: 'Username is required' }),
password: register('password', {
minLength: { value: 6, message: 'Password must be at least 6 characters' },
// password only required in add mode
validate: value => !user && !value ? 'Password is required' : undefined
})
};
async function onSubmit(data: any) {
alertService.clear();
try {
// create or update user based on user prop
let message;
if (user) {
await userService.update(user.id, data);
message = 'User updated';
} else {
await userService.create(data);
message = 'User added';
}
// redirect to user list with success message
router.push('/users');
alertService.success(message, true);
} catch (error: any) {
alertService.error(error);
}
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h1>{title}</h1>
<div className="row">
<div className="mb-3 col">
<label className="form-label">First Name</label>
<input {...fields.firstName} type="text" className={`form-control ${errors.firstName ? 'is-invalid' : ''}`} />
<div className="invalid-feedback">{errors.firstName?.message?.toString()}</div>
</div>
<div className="mb-3 col">
<label className="form-label">Last Name</label>
<input {...fields.lastName} type="text" className={`form-control ${errors.lastName ? 'is-invalid' : ''}`} />
<div className="invalid-feedback">{errors.lastName?.message?.toString()}</div>
</div>
</div>
<div className="row">
<div className="mb-3 col">
<label className="form-label">Username</label>
<input {...fields.username} type="text" className={`form-control ${errors.username ? 'is-invalid' : ''}`} />
<div className="invalid-feedback">{errors.username?.message?.toString()}</div>
</div>
<div className="mb-3 col">
<label className="form-label">
Password
{user && <em className="ms-1">(Leave blank to keep the same password)</em>}
</label>
<input {...fields.password} type="password" className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
<div className="invalid-feedback">{errors.password?.message?.toString()}</div>
</div>
</div>
<div className="mb-3">
<button type="submit" disabled={formState.isSubmitting} className="btn btn-primary me-2">
{formState.isSubmitting && <span className="spinner-border spinner-border-sm me-1"></span>}
Save
</button>
<button onClick={() => reset()} type="button" disabled={formState.isSubmitting} className="btn btn-secondary">Reset</button>
<Link href="/users" className="btn btn-link">Cancel</Link>
</div>
</form>
);
}
Alert Component
The alert component renders the alert from the alert service with bootstrap CSS classes, if the service doesn't contain an alert nothing is rendered by the component.
The useEffect()
hook clears the alert on location change by including a dependency on the current pathname
.
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { useAlertService } from '_services';
export { Alert };
function Alert() {
const pathname = usePathname();
const alertService = useAlertService();
const alert = alertService.alert;
useEffect(() => {
// clear alert on location change
alertService.clear();
}, [pathname]);
if (!alert) return null;
return (
<div className="container">
<div className="m-3">
<div className={`alert alert-dismissible ${alert.type}`}>
{alert.message}
<button type="button" className="btn-close" onClick={alertService.clear}></button>
</div>
</div>
</div>
);
}
Nav Component
The nav component displays the main navigation in the example. The custom NavLink component automatically adds the active
class to the active nav item so it is highlighted in the UI.
'use client';
import { useState } from 'react';
import { NavLink } from '_components';
import { useUserService } from '_services';
export { Nav };
function Nav() {
const [loggingOut, setLoggingOut] = useState<boolean>(false);
const userService = useUserService();
async function logout() {
setLoggingOut(true);
await userService.logout();
}
return (
<nav className="navbar navbar-expand navbar-dark bg-dark px-3">
<div className="navbar-nav">
<NavLink href="/" exact className="nav-item nav-link">Home</NavLink>
<NavLink href="/users" className="nav-item nav-link">Users</NavLink>
<button onClick={logout} className="btn btn-link nav-item nav-link" style={{ width: '67px' }} disabled={loggingOut}>
{loggingOut
? <span className="spinner-border spinner-border-sm"></span>
: <span>Logout</span>
}
</button>
</div>
</nav>
);
}
NavLink Component
An extended version of the Next.js Link
component that adds the CSS className "active"
when the href
matches the current URL. By default the href
only needs to match the start of the URL, use the exact
property to change it to an exact match (e.g. <NavLink href="/" exact>Home</NavLink>).
The INavLink
interface defines the properties for a NavLink
component. The [key: string]: any
property on the interface adds support for the rest/spread parameter (...props
) so any other attributes can be added to the returned Link
component.
'use client';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
export { NavLink };
function NavLink({ children, href, exact, ...props }: INavLink) {
const pathname = usePathname();
const isActive = exact ? pathname === href : pathname.startsWith(href);
if (isActive) {
props.className += ' active';
}
return <Link href={href} {...props}>{children}</Link>;
}
interface INavLink {
children: React.ReactNode,
href: string,
exact?: boolean,
[key: string]: any
}
Spinner Component
A simple bootstrap loading spinner component, used by the users list page and edit user page.
export { Spinner };
function Spinner() {
return (
<div className="text-center p-5">
<span className="spinner-border spinner-border-lg align-center"></span>
</div>
);
}
Fetch React Hook
The useFetch hook is a lightweight wrapper around the native browser fetch()
function used to simplify the code for making HTTP requests. It contains methods for get
, post
, put
and delete
requests, it automatically handles the parsing of JSON data from responses, and throws an error if the HTTP response is not successful (!response.ok
). If the response is 401 Unauthorized
the user is automatically logged out of the Next.js app.
The fetch hook allows you to send a POST
request as simply as this: fetch.post(url, body)
. It's used in the example app by the user service.
import { useRouter } from 'next/navigation';
export { useFetch };
function useFetch() {
const router = useRouter();
return {
get: request('GET'),
post: request('POST'),
put: request('PUT'),
delete: request('DELETE')
};
function request(method: string) {
return (url: string, body?: any) => {
const requestOptions: any = {
method
};
if (body) {
requestOptions.headers = { 'Content-Type': 'application/json' };
requestOptions.body = JSON.stringify(body);
}
return fetch(url, requestOptions).then(handleResponse);
}
}
// helper functions
async function handleResponse(response: any) {
const isJson = response.headers?.get('content-type')?.includes('application/json');
const data = isJson ? await response.json() : null;
// check for error response
if (!response.ok) {
if (response.status === 401) {
// api auto logs out on 401 Unauthorized, so redirect to login page
router.push('/account/login');
}
// get error message from body or default to response status
const error = (data && data.message) || response.statusText;
return Promise.reject(error);
}
return data;
}
}
Next.js API Handler
The API handler is a wrapper function for all API route handlers in the /app/api
folder (e.g. login route handler, register route handler).
It enables adding global middleware to the Next.js API request pipeline and adds support for global exception handling. The wrapper function accepts a handler
object that contains a method for each HTTP method that is supported by the handler (e.g. GET
, POST
, PUT
, DELETE
etc). If a request is received for an unsupported HTTP method a 405 Method Not Allowed
response is returned.
NextRequest json()
method can only be called once
The req.json()
method can only be called once to read the JSON body of an API request, which causes a problem if you need to read it twice (e.g. from middleware and from the route handler). To get around this limitation I replaced (a.k.a. monkey patched) the json()
method with a function that returns a stored copy of the JSON body from the first call.
Global API middleware
The JWT middleware verifies the auth token cookie for secure API routes.
The validate middleware validates the JSON request body against a schema (if provided). Validation schemas are created with the Joi library and attached as a .schema
property to route handler functions where required (e.g. the login route handler).
Global API exception handler
The error handler is used to handle all API exceptions in a single place.
Why not use the new Next.js 13 App Router Middleware?
I tried to implement all of the API handler functionality with the new Next.js App Router Middleware but it doesn't seem to support global error handling with a try/catch
block like below.
import { NextRequest, NextResponse } from 'next/server';
import { errorHandler, jwtMiddleware, validateMiddleware } from './';
export { apiHandler };
function apiHandler(handler: any) {
const wrappedHandler: any = {};
const httpMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
// wrap handler methods to add middleware and global error handler
httpMethods.forEach(method => {
if (typeof handler[method] !== 'function')
return;
wrappedHandler[method] = async (req: NextRequest, ...args: any) => {
try {
// monkey patch req.json() because it can only be called once
const json = await req.json();
req.json = () => json;
} catch {}
try {
// global middleware
await jwtMiddleware(req);
await validateMiddleware(req, handler[method].schema);
// route handler
const responseBody = await handler[method](req, ...args);
return NextResponse.json(responseBody || {});
} catch (err: any) {
// global error handler
return errorHandler(err);
}
};
});
return wrappedHandler;
}
Next.js API Global Error Handler
The global error handler is used catch all API errors and remove the need for duplicated error handling code throughout the Next.js tutorial API. It's added to the request pipeline in the API handler wrapper function.
By convention errors of type 'string'
are treated as custom (app specific) errors, this simplifies the code for throwing custom errors since only a string needs to be thrown (e.g. throw 'Username or password is incorrect'
), if a custom error ends with the words 'not found'
a 404
response code is returned, otherwise a standard 400
error response is returned.
If the error is an object with the name 'JsonWebTokenError'
it means JWT token validation has failed so an HTTP 401
unauthorized response code is returned with the message 'Unauthorized'
. The auth cookie is also deleted (if there is one) to automatically log the user out.
All other (unhandled) exceptions are logged to the console and return a 500
server error response code.
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
export { errorHandler };
function errorHandler(err: Error | string) {
if (typeof (err) === 'string') {
// custom application error
const is404 = err.toLowerCase().endsWith('not found');
const status = is404 ? 404 : 400;
return NextResponse.json({ message: err }, { status });
}
if (err.name === 'JsonWebTokenError') {
// jwt error - delete cookie to auto logout
cookies().delete('authorization');
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
}
// default to 500 server error
console.error(err);
return NextResponse.json({ message: err.message }, { status: 500 });
}
Next.js API JWT Middleware
The JWT middleware uses the auth helper to verify the JWT token if the request is to a secure API route (public routes are bypassed). If the token is invalid an error is thrown which causes the global error handler to return a 401 Unauthorized
response.
The middleware is added to the Next.js request pipeline in the API handler wrapper function.
The current user id is added to the request headers to make it accessible to any other code in the request (e.g. the getCurrent()
method of the users repo).
import { NextRequest } from 'next/server';
import { auth } from '_helpers/server';
export { jwtMiddleware };
async function jwtMiddleware(req: NextRequest) {
if (isPublicPath(req))
return;
// verify token in request cookie
const id = auth.verifyToken();
req.headers.set('userId', id);
}
function isPublicPath(req: NextRequest) {
// public routes that don't require authentication
const publicPaths = [
'POST:/api/account/login',
'POST:/api/account/logout',
'POST:/api/account/register'
];
return publicPaths.includes(`${req.method}:${req.nextUrl.pathname}`);
}
Next.js API Validate Middleware
The validate middleware uses the Joi library to validate JSON data in the request body against rules defined in a schema. If validation fails an error is thrown with a comma separated list of all the error messages.
Validation schemas are assigned to the schema
property of route handler functions where required (e.g. the register route handler). The req
and schema
parameters are passed by the API handler when it calls validateMiddleware()
for each request to the Next.js API. If there is no schema the middleware is bypassed.
import joi from 'joi';
export { validateMiddleware };
async function validateMiddleware(req: Request, schema: joi.ObjectSchema) {
if (!schema) return;
const options = {
abortEarly: false, // include all errors
allowUnknown: true, // ignore unknown props
stripUnknown: true // remove unknown props
};
const body = await req.json();
const { error, value } = schema.validate(body, options);
if (error) {
throw `Validation error: ${error.details.map(x => x.message).join(', ')}`;
}
// update req.json() to return sanitized req body
req.json = () => value;
}
Auth Helper
The auth helper is used to verify the JWT token in the request 'authorization'
cookie.
The verifyToken
function verifies the JWT auth token using the jsonwebtoken
library. In the example it is called from the JWT middleware. On success the decoded user id
from the auth token is returned. On fail an exception ('JsonWebTokenError'
) is thrown by the jwt.verify()
method.
The isAuthenticated
function is a simple wrapper that executes verifyToken()
in a try/catch block to return the current authentication status as a boolean (true
or false
). It is used in the secure layout and account layout components.
import jwt from 'jsonwebtoken';
import { cookies } from 'next/headers';
export const auth = {
isAuthenticated,
verifyToken
}
function isAuthenticated() {
try {
verifyToken();
return true;
} catch {
return false;
}
}
function verifyToken() {
const token = cookies().get('authorization')?.value ?? '';
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
const id = decoded.sub as string;
return id;
}
MongoDB Data Context
The MongoDB data context is used to connect to MongoDB using Mongoose and exports an object containing all of the database model objects in the application (currently only User
). It provides an easy way to read/write to any part of the database from a single point.
The database connection string (MONGODB_URI
) environment variable is defined in the .env file.
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
mongoose.connect(process.env.MONGODB_URI!);
mongoose.Promise = global.Promise;
export const db = {
User: userModel()
};
// mongoose models with schema definitions
function userModel() {
const schema = new Schema({
username: { type: String, unique: true, required: true },
hash: { type: String, required: true },
firstName: { type: String, required: true },
lastName: { type: String, required: true }
}, {
// add createdAt and updatedAt timestamps
timestamps: true
});
schema.set('toJSON', {
virtuals: true,
versionKey: false,
transform: function (doc, ret) {
delete ret._id;
delete ret.hash;
}
});
return mongoose.models.User || mongoose.model('User', schema);
}
MongoDB Users Repo
The users repo encapsulates all access to user data stored in MongoDB, it exposes methods for authentication and standard CRUD operations for reading and managing user data. The repo is used on the server-side by the Next.js users and account API route handlers (e.g. account login route, users [id] route).
User authentication
The authenticate
method verifies the provided username and password. On success a JWT (JSON Web Token) is generated with the jsonwebtoken
npm package, the token is digitally signed using a secret key (JWT_SECRET
) so it can't be tampered with, the jwt secret is defined in the .env file.
The JWT is returned to the client in an HTTP Only 'authorization'
cookie by the login route handler, the cookie is then automatically sent with subsequent requests from the browser to the API.
Password encryption
Bcrypt is used to hash and verify passwords in the Next.js tutorial with the bcryptjs
library, for more info see Node.js - Hash and Verify Passwords with Bcrypt.
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import { headers } from 'next/headers';
import { db } from './db';
const User = db.User;
export const usersRepo = {
authenticate,
getAll,
getById,
getCurrent,
create,
update,
delete: _delete
};
async function authenticate({ username, password }: { username: string, password: string }) {
const user = await User.findOne({ username });
if (!(user && bcrypt.compareSync(password, user.hash))) {
throw 'Username or password is incorrect';
}
// create a jwt token that is valid for 7 days
const token = jwt.sign({ sub: user.id }, process.env.JWT_SECRET!, { expiresIn: '7d' });
return {
user: user.toJSON(),
token
};
}
async function getAll() {
return await User.find();
}
async function getById(id: string) {
try {
return await User.findById(id);
} catch {
throw 'User Not Found';
}
}
async function getCurrent() {
try {
const currentUserId = headers().get('userId');
return await User.findById(currentUserId);
} catch {
throw 'Current User Not Found';
}
}
async function create(params: any) {
// validate
if (await User.findOne({ username: params.username })) {
throw 'Username "' + params.username + '" is already taken';
}
const user = new User(params);
// hash password
if (params.password) {
user.hash = bcrypt.hashSync(params.password, 10);
}
// save user
await user.save();
}
async function update(id: string, params: any) {
const user = await User.findById(id);
// validate
if (!user) throw 'User not found';
if (user.username !== params.username && await User.findOne({ username: params.username })) {
throw 'Username "' + params.username + '" is already taken';
}
// hash password if it was entered
if (params.password) {
params.hash = bcrypt.hashSync(params.password, 10);
}
// copy params properties to user
Object.assign(user, params);
await user.save();
}
async function _delete(id: string) {
await User.findByIdAndRemove(id);
}
Alert Service React Hook
The alert service is a React hook that acts as the bridge between any component in the Next.js tutorial app and the alert component that displays notifications. It contains methods for displaying and clearing alerts, and an alert
property to access the current alert.
Displaying an alert
You can trigger an alert notification from any component in the application by calling one of the methods for displaying different types of alerts (success()
and error()
).
Alert method parameters
- The first parameter is the alert
message
string. - The second is an optional
showAfterRedirect
boolean parameter that keeps the alert displayed after one route change (e.g. the redirect after successful registration). Default value isfalse
.
Zustand state management
Zustand is used internally by the alert service to manage shared state data (alert
) so it can be accessed by any component in the application. It's a light-weight alternative to Redux for React state management, for more info see https://docs.pmnd.rs/zustand/getting-started/introduction.
TypeScript interfaces
TypeScript interfaces are used to define the properties and methods of an alert object (IAlert
), the zustand alert state store (IAlertStore
), and the alert service (IAlertService
). The alert service interface extends the alert store interface to include the alert
property in service interface.
import { create } from 'zustand';
export { useAlertService };
// alert state store
const alertStore = create<IAlertStore>(() => ({}));
function useAlertService(): IAlertService {
const { alert } = alertStore();
return {
alert,
success: (message: string, showAfterRedirect = false) => {
const type = 'alert-success';
alertStore.setState({
alert: { type, message, showAfterRedirect }
});
},
error: (message: string, showAfterRedirect = false) => {
const type = 'alert-danger';
alertStore.setState({
alert: { type, message, showAfterRedirect }
});
},
clear: () => {
alertStore.setState(state => {
let alert = state.alert;
// if showAfterRedirect is true the alert is kept for
// one route change (e.g. after successful registration)
if (alert?.showAfterRedirect) {
alert.showAfterRedirect = false;
} else {
alert = undefined;
}
return { alert };
});
}
}
}
// interfaces
interface IAlert {
type: string,
message: string,
showAfterRedirect: boolean
}
interface IAlertStore {
alert?: IAlert
}
interface IAlertService extends IAlertStore {
success: (message: string, showAfterRedirect?: boolean) => void,
error: (message: string, showAfterRedirect?: boolean) => void,
clear: () => void,
}
User Service React Hook
The user service is a React hook that encapsulates client-side logic and handles HTTP communication between the React front-end and the Next.js back-end API for everything related to users.
It contains methods for logging in and out of the app, registering a new user, and standard CRUD methods for retrieving, creating and updating user data. HTTP requests are sent with the help of the useFetch hook.
Zustand state management
Zustand is a light-weight React state management library used internally by the user service to manage shared state properties (users
, user
and currentUser
) so they can be accessed by any component in the application.
For example the users list page displays all users by calling userService.getAll()
on load and reading the userService.users
property. The getAll()
method fetches all users from the API and assigns them to the users
shared state property (with userStore.setState()
) which triggers a re-render of the page.
The getById()
and getCurrent()
methods work the same way by fetching and setting the user
and currentUser
shared state properties.
TypeScript interfaces
TypeScript interfaces are used to define the properties and methods of a user object (IUser
), the zustand user state store (IUserStore
), and the user service (IUserService
). The user service interface extends the user store interface to include the shared state properties (users
, user
and currentUser
) in service interface.
import { create } from 'zustand';
import { useRouter, useSearchParams } from 'next/navigation';
import { useAlertService } from '_services';
import { useFetch } from '_helpers/client';
export { useUserService };
// user state store
const initialState = {
users: undefined,
user: undefined,
currentUser: undefined
};
const userStore = create<IUserStore>(() => initialState);
function useUserService(): IUserService {
const alertService = useAlertService();
const fetch = useFetch();
const router = useRouter();
const searchParams = useSearchParams();
const { users, user, currentUser } = userStore();
return {
users,
user,
currentUser,
login: async (username, password) => {
alertService.clear();
try {
const currentUser = await fetch.post('/api/account/login', { username, password });
userStore.setState({ ...initialState, currentUser });
// get return url from query parameters or default to '/'
const returnUrl = searchParams.get('returnUrl') || '/';
router.push(returnUrl);
} catch (error: any) {
alertService.error(error);
}
},
logout: async () => {
await fetch.post('/api/account/logout');
router.push('/account/login');
},
register: async (user) => {
try {
await fetch.post('/api/account/register', user);
alertService.success('Registration successful', true);
router.push('/account/login');
} catch (error: any) {
alertService.error(error);
}
},
getAll: async () => {
userStore.setState({ users: await fetch.get('/api/users') });
},
getById: async (id) => {
userStore.setState({ user: undefined });
try {
userStore.setState({ user: await fetch.get(`/api/users/${id}`) });
} catch (error: any) {
alertService.error(error);
}
},
getCurrent: async () => {
if (!currentUser) {
userStore.setState({ currentUser: await fetch.get('/api/users/current') });
}
},
create: async (user) => {
await fetch.post('/api/users', user);
},
update: async (id, params) => {
await fetch.put(`/api/users/${id}`, params);
// update current user if the user updated their own record
if (id === currentUser?.id) {
userStore.setState({ currentUser: { ...currentUser, ...params } })
}
},
delete: async (id) => {
// set isDeleting prop to true on user
userStore.setState({
users: users!.map(x => {
if (x.id === id) { x.isDeleting = true; }
return x;
})
});
// delete user
const response = await fetch.delete(`/api/users/${id}`);
// remove deleted user from state
userStore.setState({ users: users!.filter(x => x.id !== id) });
// logout if the user deleted their own record
if (response.deletedSelf) {
router.push('/account/login');
}
}
}
};
// interfaces
interface IUser {
id: string,
firstName: string,
lastName: string,
username: string,
password: string,
isDeleting?: boolean
}
interface IUserStore {
users?: IUser[],
user?: IUser,
currentUser?: IUser
}
interface IUserService extends IUserStore {
login: (username: string, password: string) => Promise<void>,
logout: () => Promise<void>,
register: (user: IUser) => Promise<void>,
getAll: () => Promise<void>,
getById: (id: string) => Promise<void>,
getCurrent: () => Promise<void>,
create: (user: IUser) => Promise<void>,
update: (id: string, params: Partial<IUser>) => Promise<void>,
delete: (id: string) => Promise<void>
}
Next.js Login Form
The login page contains a form built with the React Hook Form library that contains username and password fields for logging into the Next.js tutorial app.
The useForm()
hook function returns an object with methods for working with a form including registering inputs, handling form submit, resetting the form, accessing form state, displaying errors and more, for a complete list see https://react-hook-form.com/docs/useform.
Form validation rules are defined with options passed to the React Hook Form register()
function that is used to register each form input field. For a full list of validation options available see https://react-hook-form.com/docs/useform/register#options.
The onSubmit
function gets called when the form is submitted and valid, and submits the user credentials to the API by calling userService.login()
.
The returned TSX/JSX template contains the form with all of the input fields and validation messages. The form fields are bound to each input element with the JS spread operater (e.g. {...fields.username}
).
'use client';
import Link from 'next/link';
import { useForm } from 'react-hook-form';
import { useUserService } from '_services';
export default Login;
function Login() {
const userService = useUserService();
// get functions to build form with useForm() hook
const { register, handleSubmit, formState } = useForm();
const { errors } = formState;
const fields = {
username: register('username', { required: 'Username is required' }),
password: register('password', { required: 'Password is required' })
};
async function onSubmit({ username, password }: any) {
await userService.login(username, password);
}
return (
<div className="card">
<h4 className="card-header">Login</h4>
<div className="card-body">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-3">
<label className="form-label">Username</label>
<input {...fields.username} type="text" className={`form-control ${errors.username ? 'is-invalid' : ''}`} />
<div className="invalid-feedback">{errors.username?.message?.toString()}</div>
</div>
<div className="mb-3">
<label className="form-label">Password</label>
<input {...fields.password} type="password" className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
<div className="invalid-feedback">{errors.password?.message?.toString()}</div>
</div>
<button disabled={formState.isSubmitting} className="btn btn-primary">
{formState.isSubmitting && <span className="spinner-border spinner-border-sm me-1"></span>}
Login
</button>
<Link href="/account/register" className="btn btn-link">Register</Link>
</form>
</div>
</div>
);
}
Next.js Registration Form
The register page contains a simple registration form built with the React Hook Form library with fields for first name, last name, username and password.
The useForm()
hook function returns an object with methods for working with a form including registering inputs, handling form submit, resetting the form, accessing form state, displaying errors and more, for a complete list see https://react-hook-form.com/docs/useform.
Form validation rules are defined with options passed to the React Hook Form register()
function that is used to register each form input field. For a full list of validation options available see https://react-hook-form.com/docs/useform/register#options.
The onSubmit
function gets called when the form is submitted and valid, and submits the form data to the Next.js API by calling userService.register()
.
The returned TSX/JSX template contains the form with all of the input fields and validation messages. The form fields are bound to each input element with the JS spread operater (e.g. {...fields.username}
).
'use client';
import Link from 'next/link';
import { useForm } from 'react-hook-form';
import { useUserService } from '_services';
export default Register;
function Register() {
const userService = useUserService();
// get functions to build form with useForm() hook
const { register, handleSubmit, formState } = useForm();
const { errors } = formState;
const fields = {
firstName: register('firstName', { required: 'First Name is required' }),
lastName: register('lastName', { required: 'Last Name is required' }),
username: register('username', { required: 'Username is required' }),
password: register('password', {
required: 'Password is required',
minLength: { value: 6, message: 'Password must be at least 6 characters' }
})
}
async function onSubmit(user: any) {
await userService.register(user);
}
return (
<div className="card">
<h4 className="card-header">Register</h4>
<div className="card-body">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-3">
<label className="form-label">First Name</label>
<input {...fields.firstName} type="text" className={`form-control ${errors.firstName ? 'is-invalid' : ''}`} />
<div className="invalid-feedback">{errors.firstName?.message?.toString()}</div>
</div>
<div className="mb-3">
<label className="form-label">Last Name</label>
<input {...fields.lastName} type="text" className={`form-control ${errors.lastName ? 'is-invalid' : ''}`} />
<div className="invalid-feedback">{errors.lastName?.message?.toString()}</div>
</div>
<div className="mb-3">
<label className="form-label">Username</label>
<input {...fields.username} type="text" className={`form-control ${errors.username ? 'is-invalid' : ''}`} />
<div className="invalid-feedback">{errors.username?.message?.toString()}</div>
</div>
<div className="mb-3">
<label className="form-label">Password</label>
<input {...fields.password} type="password" className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
<div className="invalid-feedback">{errors.password?.message?.toString()}</div>
</div>
<button disabled={formState.isSubmitting} className="btn btn-primary">
{formState.isSubmitting && <span className="spinner-border spinner-border-sm me-1"></span>}
Register
</button>
<Link href="/account/login" className="btn btn-link">Cancel</Link>
</form>
</div>
</div>
);
}
Next.js Account Layout
The account layout component contains common layout code for all pages in the /app/(public)/account
folder, it contains an <Alert />
component above the {children}
elements which are wrapped in a div
with some bootstrap classes to set the width and alignment of all account pages.
Auth status is checked at the top to automatically redirect the user to the home page if they are already logged in.
import { redirect } from 'next/navigation';
import { auth } from '_helpers/server';
import { Alert } from '_components';
export default Layout;
function Layout({ children }: { children: React.ReactNode }) {
// if logged in redirect to home page
if (auth.isAuthenticated()) {
redirect('/');
}
return (
<>
<Alert />
<div className="col-md-6 offset-md-3 mt-5">
{children}
</div>
</>
);
}
Add User Page
The add user page simply renders the add/edit user component without any user
specified so the component is set to "add" mode.
import { AddEdit } from '_components/users';
export default Add;
function Add() {
return <AddEdit title="Add User" />;
}
Edit User Page
The edit user page renders the add/edit user component with the specified user
so the component is set to "edit" mode. The user is fetched from the API in a useEffect()
React hook with the id
parameter from the URL, the id is accessible via the component params.
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { AddEdit } from '_components/users';
import { Spinner } from '_components';
import { useUserService } from '_services';
export default Edit;
function Edit({ params: { id } }: any) {
const router = useRouter();
const userService = useUserService();
const user = userService.user;
useEffect(() => {
if (!id) return;
// fetch user for add/edit form
userService.getById(id)
}, [router]);
return user
? <AddEdit title="Edit User" user={user} />
: <Spinner />;
}
Users List Page
The users list page displays a list of all users in the Next.js tutorial app and contains buttons for adding, editing and deleting users.
All users are fetched in a useEffect()
hook with the user service, and accessed via the userService.users
property.
The delete button calls the userService.deleteUser()
method. The method sets the user isDeleting
property so the UI displays a spinner on the delete button while the user is deleted from the API. If the current user deletes their own record they are automatically logged out of the app.
'use client';
import Link from 'next/link';
import { useEffect } from 'react';
import { Spinner } from '_components';
import { useUserService } from '_services';
export default Users;
function Users() {
const userService = useUserService();
const users = userService.users;
useEffect(() => {
userService.getAll();
}, []);
return (
<>
<h1>Users</h1>
<Link href="/users/add" className="btn btn-sm btn-success mb-2">Add User</Link>
<table className="table table-striped">
<thead>
<tr>
<th style={{ width: '30%' }}>First Name</th>
<th style={{ width: '30%' }}>Last Name</th>
<th style={{ width: '30%' }}>Username</th>
<th style={{ width: '10%' }}></th>
</tr>
</thead>
<tbody>
<TableBody />
</tbody>
</table>
</>
);
function TableBody() {
if (users?.length) {
return (users.map(user =>
<tr key={user.id}>
<td>{user.firstName}</td>
<td>{user.lastName}</td>
<td>{user.username}</td>
<td style={{ whiteSpace: 'nowrap' }}>
<Link href={`/users/edit/${user.id}`} className="btn btn-sm btn-primary me-1">Edit</Link>
<button onClick={() => userService.delete(user.id)} className="btn btn-sm btn-danger btn-delete-user" style={{ width: '60px' }} disabled={user.isDeleting}>
{user.isDeleting
? <span className="spinner-border spinner-border-sm"></span>
: <span>Delete</span>
}
</button>
</td>
</tr>
));
}
if (!users) {
return (
<tr>
<td colSpan={4}>
<Spinner />
</td>
</tr>
);
}
if (users?.length === 0) {
return (
<tr>
<td colSpan={4} className="text-center">
<div className="p-2">No Users To Display</div>
</td>
</tr>
);
}
}
}
Next.js Secure Layout
The secure layout component contains common layout code for all pages in the /app/(secure)
folder, it contains the main <Nav />
component and an <Alert />
component above the {children}
elements which are wrapped in a couple of divs with bootstrap classes to set the padding and alignment of all of the secure pages.
Auth status is checked at the top to automatically redirect unauthenticated users to the login page along with the request path in the query params (returnUrl
). The request path is accessible in server components via the Next.js request header x-invoke-path
.
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { auth } from '_helpers/server';
import { Alert, Nav } from '_components';
export default Layout;
function Layout({ children }: { children: React.ReactNode }) {
// if not logged in redirect to login page
if (!auth.isAuthenticated()) {
const returnUrl = encodeURIComponent(headers().get('x-invoke-path') || '/');
redirect(`/account/login?returnUrl=${returnUrl}`);
}
return (
<div className="app-container bg-light">
<Nav />
<Alert />
<div className="p-4">
<div className="container">
{children}
</div>
</div>
</div>
);
}
Home Page
The home page is a simple React component that displays a welcome message with the logged in user's name and a link to the users section.
The current user is fetched in a useEffect()
hook with the user service, and accessed via the userService.currentUser
property.
'use client';
import Link from 'next/link';
import { useEffect } from 'react';
import { useUserService } from '_services';
import { Spinner } from '_components';
export default Home;
function Home() {
const userService = useUserService();
const user = userService.currentUser;
useEffect(() => {
userService.getCurrent();
}, []);
if (user) {
return (
<>
<h1>Hi {user.firstName}!</h1>
<p>You're logged in with Next.js & JWT!!</p>
<p><Link href="/users">Manage Users</Link></p>
</>
);
} else {
return <Spinner />;
}
}
Next.js API Login Route
The login route handler receives HTTP requests sent to the login route /api/account/login
. It supports HTTP POST
requests containing a username and password which are authenticated by the usersRepo.authenticate()
function. On success a JWT auth token is set in an HTTP only 'authorization'
cookie.
The route handler supports HTTP POST
requests by passing an object with a POST()
method to the apiHandler() which is mapped to the login()
function.
The request schema is defined with the joi
data validation library and assigned to the login.schema
property. Schema validation is handled by the validate middleware which is called by the api handler.
import { cookies } from 'next/headers';
import joi from 'joi';
import { usersRepo } from '_helpers/server';
import { apiHandler } from '_helpers/server/api';
module.exports = apiHandler({
POST: login
});
async function login(req: Request) {
const body = await req.json();
const { user, token } = await usersRepo.authenticate(body);
// return jwt token in http only cookie
cookies().set('authorization', token, { httpOnly: true });
return user;
}
login.schema = joi.object({
username: joi.string().required(),
password: joi.string().required()
});
Next.js API Logout Route
The logout route simply deletes the 'authorization'
cookie to log the user out. The cookie is HTTP only which means it is only accessible on the server, this improves security by preventing cross site scripting (XSS) where malicious client side code can attempt to access private data like cookies.
To logout the client side user service sends a request to this API route and then redirects to the login page.
import { cookies } from 'next/headers';
import { apiHandler } from '_helpers/server/api';
module.exports = apiHandler({
POST: logout
});
function logout() {
cookies().delete('authorization');
}
Next.js API Register Route
The register handler receives HTTP requests sent to the register route /api/account/register
. It supports HTTP POST
requests containing user details which are registered in the Next.js tutorial app by the usersRepo.create()
method.
The route handler supports HTTP POST
requests by passing an object with a POST()
method to the apiHandler() function.
The request schema is defined with the joi
data validation library and assigned to the register.schema
property. Schema validation is handled by the validate middleware which is called by the api handler.
import joi from 'joi';
import { usersRepo } from '_helpers/server';
import { apiHandler } from '_helpers/server/api';
module.exports = apiHandler({
POST: register
});
async function register(req: Request) {
const body = await req.json();
await usersRepo.create(body);
}
register.schema = joi.object({
firstName: joi.string().required(),
lastName: joi.string().required(),
username: joi.string().required(),
password: joi.string().min(6).required(),
});
Users [id]
API Route
A dynamic API route handler that handles HTTP requests with any value as the [id]
parameter (i.e. /api/users/*
). The user id
parameter is attached by Next.js to the params
object which is passed to the route handler.
The route handler supports HTTP GET
, PUT
and DELETE
requests by passing an object with those method names to the apiHandler() which maps them to the functions getById()
, update()
and _delete()
.
The request schema for the update()
function is defined with the joi
data validation library and assigned to the update.schema
property. Schema validation is handled by the validate middleware which is called by the api handler.
import joi from 'joi';
import { cookies } from 'next/headers';
import { apiHandler } from '_helpers/server/api';
import { usersRepo } from '_helpers/server';
module.exports = apiHandler({
GET: getById,
PUT: update,
DELETE: _delete
});
async function getById(req: Request, { params: { id } }: any) {
return await usersRepo.getById(id);
}
async function update(req: Request, { params: { id } }: any) {
const body = await req.json();
await usersRepo.update(id, body);
}
update.schema = joi.object({
firstName: joi.string(),
lastName: joi.string(),
username: joi.string(),
password: joi.string().min(6).allow(''),
});
async function _delete(req: Request, { params: { id } }: any) {
await usersRepo.delete(id);
// auto logout if deleted self
if (id === req.headers.get('userId')) {
cookies().delete('authorization');
return { deletedSelf: true };
}
}
Current User API Route
The current user handler receives HTTP requests sent to the current user route /api/users/current
. It supports HTTP GET
requests by passing an object with that method name to the apiHandler() which maps it to the getCurrent()
function.
The getCurrent()
function returns the current logged in user details from MongoDB by calling usersRepo.getCurrent()
.
import { usersRepo } from '_helpers/server';
import { apiHandler } from '_helpers/server/api';
module.exports = apiHandler({
GET: getCurrent
});
async function getCurrent() {
return await usersRepo.getCurrent();
}
Users API Route
The users handler receives HTTP requests sent to the base users route /api/users
. It supports HTTP GET
and POST
requests by passing an object with those method names to the apiHandler() which maps them to the functions getAll()
and create()
.
The getAll()
function returns all users from MongoDB by calling usersRepo.getAll()
.
The create()
function creates a new user in MongoDB by calling usersRepo.create()
.
The request schema for the create()
function is defined with the joi
data validation library and assigned to the create.schema
property. Schema validation is handled by the validate middleware which is called by the api handler.
Security for this route (and all other secure routes in the Next.js API) is handled by the global JWT middleware.
import joi from 'joi';
import { usersRepo } from '_helpers/server';
import { apiHandler } from '_helpers/server/api';
module.exports = apiHandler({
GET: getAll,
POST: create
});
async function getAll() {
return await usersRepo.getAll();
}
async function create(req: Request) {
const body = await req.json();
await usersRepo.create(body);
}
create.schema = joi.object({
firstName: joi.string().required(),
lastName: joi.string().required(),
username: joi.string().required(),
password: joi.string().min(6).required(),
});
Global CSS
The globals.css file contains global custom CSS styles for the Next.js example auth app. It's imported into the tutorial app by the root layout component.
.app-container { min-height: 350px; }
Next.js Root Layout
The root layout component contains the outer <html>
and <body>
tags for the Next.js app, SEO metadata elements in the <head>
tag (e.g. title and description) are controlled with the exported metadata
object. Global CSS stylesheets are imported at the top of the file.
import 'bootstrap/dist/css/bootstrap.min.css';
import 'globals.css';
export const metadata = {
title: 'Next.js 13 - User Registration and Login Example'
}
export default Layout;
function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
</body>
</html>
);
}
Dotenv (.env) file
The .env
file contains environment variables that are accessible to code in the Next.js app via the process.env
object.
By default environment variables are only accessible to server-side code, to make env vars accessible to the React client app they must be prefixed with NEXT_PUBLIC_
. For more info see Next.js - Access Environment Variables from dotenv (.env).
Multiple dotenv files can be created to configure different environments. .env
is the base file that applies to all environments, you can also create .env.development
for dev specific variables, .env.production
for prod and .env.local
for the local environment.
IMPORTANT: The JWT_SECRET
environment variable is used to sign and verify JWT tokens for authentication, change it with your own random string to ensure nobody else can generate a JWT with the same secret to gain unauthorized access to your Next.js app or API. A quick and easy way is join a couple of GUIDs together to make a long random string (e.g. from https://www.guidgenerator.com/).
JWT_SECRET=THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING
MONGODB_URI=mongodb://127.0.0.1/next-js-registration-login-example
ESLint Config
The ESLint RC file configures which rules are enabled when npm run lint
or next lint
is run on the project.
The default rule set is next/core-web-vitals
, this is included when a Next.js app is created with the command npx create-next-app@latest
. For more info on create-next-app
see https://nextjs.org/docs/app/api-reference/create-next-app.
{
"extends": "next/core-web-vitals"
}
Next.js Config
The Next.js config file can be used to configure different options for the Next.js project.
The file was automatically generated when I created the Next.js base project with the command npx create-next-app@latest
and I haven't made any changes. For info on all of the config options available see https://nextjs.org/docs/app/api-reference/next-config-js
/** @type {import('next').NextConfig} */
const nextConfig = {}
module.exports = nextConfig
Package.json
The package.json file contains project configuration information including scripts for running and building the Next.js tutorial app, and dependencies that get installed when you run npm install
or npm i
. Full documentation is available on the npm docs website.
For more info on the Next.js CLI commands used in the package.json scripts see https://nextjs.org/docs/app/api-reference/next-cli.
{
"name": "next-js-13-app-router-mongodb-rego-login-example",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "20.4.5",
"@types/react": "18.2.16",
"@types/react-dom": "18.2.7",
"bcryptjs": "^2.4.3",
"bootstrap": "^5.3.0",
"eslint": "8.45.0",
"eslint-config-next": "13.4.12",
"joi": "^17.9.2",
"jsonwebtoken": "^9.0.1",
"mongodb": "^5.7.0",
"mongoose": "^7.4.1",
"next": "13.4.12",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.45.2",
"typescript": "5.1.6",
"zustand": "^4.3.9"
}
}
TypeScript Config
The tsconfig file contains compiler options and configuration that specify how the TypeScript project is compiled into JavaScript.
The file was automatically generated when I created the base Next.js project with the command npx create-next-app@latest
with the TypeScript option. The only change I made was adding the baseUrl
.
The baseUrl
option is used to configure absolute imports for the Next.js tutorial app. Setting the base url to "./app"
makes all TypeScript import statements (without a dot '.' prefix) relative to the app folder of the project, removing the need for long relative paths like import { userService } from '../../../_services';
.
Next.js also supports module path aliases(e.g. '@/_services') in the tsconfig file, for more info see https://nextjs.org/docs/app/building-your-application/configuring/absolute-imports-and-module-aliases.
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
// make all imports without a dot '.' prefix relative to the app directory
"baseUrl": "./app"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
Other versions of this tutorial
The registration and login tutorial is also available in the following versions:
- Next.js: Next.js 13 (Pages Router) + MongoDB, Next.js 13 (Pages Router) + MySQL, Next.js 11
- React: React 18 + Redux, React 17 + Recoil, React 16 Hooks + Redux, React 16 + Redux
- Angular: Angular 15/16, 14, 10, 9, 8, 7, 6, 2/5
- Vue: Vue 3 + Pinia, Vue 2 + Vuex
- AngularJS: AngularJS
- .NET: Blazor WebAssembly
Need Some NextJS Help?
Search fiverr for freelance NextJS developers.
Follow me for updates
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!