Boilerplate for MERN stack development with TypeScript and Docker.
Modern, clean, and consistent TypeScript boilerplate that incorporates best practices for Node.js and React, keeping scalability and performance in mind. The advanced structure enables the development of applications of any size. It includes authentication and end-to-end tests, with a script for the quick setup.
You can support the project on ko-fi.
- JWT Authentication
- TypeScript on both ends
- Modern EcmaScript features
- Clean, consistent code
- Folder by Features structure
- Simplified production builds
- Docker containers
- Automated setup
- End-to-end tests
- Node v18
- TypeScript
- Express framework
- MongoDB (Mongoose)
- Mocha for testing
- EJS for rendering
- Nodemailer for emails
- Winston for logging
- JWT authentication
- Socket.IO for real-time connection
- Folder by Feature structure
- Clean, consistent code using Prettier
- Using ES6 and ES8 features
- Webpack build for production
- Docker or PM2 for production
- React v18
- TypeScript
- Using Hooks
- Jest, Cypress for testing
- Node-Sass for SCSS support
- Redux Toolkit for global state management
- Built-in routing (react-router)
- Socket.IO for real-time connection
- Folder by Feature structure
- Clean, consistent code using Prettier
- Using ES6 and ES8 features
- Light and Dark themes (SCSS)
- Toast notifications (react-toastify)
- Animations (animate.css)
- Fontawesome/material icons
These are necessary steps before setting up the project.
An automated project setup script is available. If you will use it, you can skip the SSL and JWT setup, but you will still need to setup a database and a mailbox.
git clone [email protected]:tamasszoke/mern-seed.git
Or download it as a
.zip
file.
You will need an SSL certificate for secure connection.
Generate a self-signed certificate using OpenSSL:
One-liner: openssl req -x509 -sha256 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -days 365
To skip the questions, add
-subj '/CN=localhost'
to the end of the line above.
Or
- Generate your private key:
openssl genrsa -out key.pem
- Generate your csr:
openssl req -new -key key.pem -out csr.pem
- Generate your certificate:
openssl x509 -sha256 -req -days 365 -in csr.pem -signkey key.pem -out cert.pem
Repeat these steps for the production certificate.
Tip: use key.prod.pem and cert.prod.pem for filenames.
You will need an RSA key and secret to use JWT tokens securely.
To generate it locally, use the following:
- Generate secret key:
openssl genrsa -out jwt.secret.pem 2048
- Generate public key:
openssl rsa -in jwt.secret.pem -outform PEM -pubout -out jwt.public.pem
You can also generate those using online tools such as the jsencript demo:
- Go to jsencript demo
- Click on generate new keys button
- Save the keys as
jwt.secret.pem
andjwt.public.pem
files
Repeat these steps for the production RSA keys.
Tip: use jwt.secret.prod.pem and jwt.public.prod.pem for filenames.
This project was prepared to use MongoDB.
You can also use any database, just follow the steps below.
Follow these steps to create a free Atlas shared database from the official MongoDB Atlas tutorial:
- Create a MongoDB Cloud account
- Create a MongoDB Atlas cluster
- Configure network access and create a cluster user
- Connect to the cluster
If you want to use a relational or any other database, follow these steps:
- Install the appropriate package on backend
- Configure the .env files in
./backend/.env/
folder - Set up the connection in
./backend/src/core/config/components/database.ts
file - Adjust the code in
./backend/src/features/auth/
folder
You will need a service to test emails without actually sending the messages. There are services like MailSlurp, MailHog, MailNest, Mailsnag, and Mailtrap. This project uses MailTrap.
Create an account on MailTrap and follow these steps:
- Add a new inbox and go to it's smtp settings
- Choose the nodemailer integration example
- Copy the data to the .env files on backend
You can also create a local SMTP server to send and receive emails, but this requires more work to get it running, try the smtp-tester package.
Don't forget to setup a database and a mailbox before using the script!
Run ./scripts/setup.sh
from the root folder and follow the instructions.
If it's ready, you can jump to the usage section!
- Install dependencies
- Add SSL files
- Add JWT files
- Set env variables
Run yarn install
inside the backend folder.
Run yarn install
inside the frontend folder.
Run
yarn install
inside the root folder.
Put your cert.pem
and key.pem
files inside the ./backend/.ssl/
folder.
If you don't have these files, you can generate a self-signed SSL certificate, check the SSL setup section.
Put your jwt.secret.pem
and jwt.public.pem
files inside the ./backend/.jwt/
folder.
If you don't have these files, check the JWT setup section to generate them.
Create .env.development
and .env.production
files inside the ./backend/.env/
folder.
Use port 3001 in development and port 8080 in production. On the frontend use ports 3000 and 8080.
Example for development (include all):
HOST=0.0.0.0
PORT=3001
FRONTEND_HOST=localhost
FRONTEND_PORT=3000
SSL_KEY=[SSL_KEY_FILE_NAME]
SSL_CRT=[SSL_CRT_FILE_NAME]
JWT_SECRET=[JWT_SECRET_KEY_FILE_NAME]
JWT_PUBLIC=[JWT_PUBLIC_KEY_FILE_NAME]
COOKIE_SECRET=[COOKIE_SECRET_KEY]
DATABASE_URL=[MONGOLAB_DB_URL]
EMAIL_USER=[EMAIL_USER]
EMAIL_PASS=[EMAIL_PASSWORD]
EMAIL_FROM=[EMAIL_FROM]
Located at ./backend/.env/.env.development
.
If you change the ports, change them in the other env files too (root, frontend, cypress).
Create .env.development
and .env.production
files inside the ./frontend/
folder.
Use port 3000 in development and port 8080 in production. On the backend use ports 3001 and 8080.
Example (include all):
REACT_APP_HOST=0.0.0.0
REACT_APP_PORT=3000
REACT_APP_BACKEND_HOST=localhost
REACT_APP_BACKEND_PORT=3001
SKIP_PREFLIGHT_CHECK=true
CHOKIDAR_USEPOLLING=true
Located at ./frontend/.env.development
.
If you change the ports, change them in the other env files too (root, backend, cypress).
Create .cypress.env.json
file inside the ./frontend/
folder.
Example (include all):
{
EMAIL_API_URL="https://mailtrap.io/api/accounts/[YOUR_ACCOUNT_ID]/inboxes/[YOUR_INBOX_ID]/messages",
EMAIL_API_TOKEN="[YOUR_API_TOKEN]",
}
You will also need to set up the correct hostname and port number for Cypress in the config file located at ./frontend/cypress.config.ts
.
Example config file:
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'https://localhost:3000',
},
})
If you change the ports, change them in the other env files too (root, backend, frontend).
Create .env
file inside the root folder.
Use ports 3000/3001 for development and port 8080 for production.
Example (include all):
DEVELOPMENT_BACKEND_PORT=3001
DEVELOPMENT_FRONTEND_PORT=3000
PRODUCTION_PORT=8080
Located at ./.env
.
If you change the ports, change them in the other env files too (backend, frontend, cypress).
Use the following commands from the root folder.
- Start
yarn docker:dev
ordocker-compose -f docker-compose.development.yml up
- Backend at https://localhost:3001
- Frontend at https://localhost:3000
Stop the container using docker-compose -f docker-compose.development.yml down
Run
npm rebuild node-sass
inside the frontend container if asked.
Type the famous 'thisisunsafe' message if you got browser security error.
- Run
yarn build
- Start
yarn docker:prod
ordocker-compose -f docker-compose.production.yml up
- Go to https://localhost:8080
Stop the container using docker-compose -f docker-compose.production.yml down
.
- Install the necessary tools by running
yarn install
from the root folder - Set the environment you use in the
./backend/package.json
file
Add the NODE_ENV variables to the start of the scripts:
"start": "NODE_ENV=development ..."
"prod": "NODE_ENV=production ..."
Note: on Windows you may need to use
set NODE_ENV=development && ...
.
- Run
yarn start
- Backend at https://localhost:3001
- Frontend at https://localhost:3000
- Run
yarn build
from the root folder - Run
yarn install
inside the build folder - Start with
yarn prod
from the root folder - Go to https://localhost:8080
PM2 support
Start the build with yarn prod:pm2
.
Remove all instances using yarn prod:pm2:kill
.
You can use the following scripts from the root folder (yarn/npm
):
Name | Description |
---|---|
test:backend | Run the backend tests |
test:frontend | Run the frontend tests |
test:e2e | Run the end-to-end tests |
start | Don't forget to run yarn install first from the root folder |
start:backend | Run the node server in development mode |
start:frontend | Run the react client in development mode |
build | Build the complete application for production to the build folder |
prod | Run the built app in production mode with node (build only) |
prod:pm2 | Run the built app in production mode with PM2 (build only) |
prod:pm2:kill | Stop the instances of PM2 (build only) |
docker:dev | Start with docker in development mode |
docker:prod | Start with docker in production mode |
Check backend test coverage using
yarn test:coverage
from the backend folder.
As a bonus, I've made a some scripts in bash to help you get started faster.
Find them in the ./scripts/
folder:
Name | Description |
---|---|
setup.sh | Interactive project setup |
build.sh | Create a clean build |
The setup script will ask for the necessary keys and configurations and then create the appropriate files. The build script will remove the existing build directories and create the new build.
The most important files of are in the ./src/core/
folder, the server won't work without them. There are optional files in the ./src/common/
and ./src/features/
folders, these are not necessary to run the server.
The ./src/index.ts
is the main file, where the server's listen function will load the configuration and start the database connection, the routes, the server, and the Socket.IO server.
Similarly to the backend, find the important files in the ./src/core/
folder, while the optional files are in the ./src/common/
and ./src/features/
folders.
Based on the create-react-app using the Redux Toolkit, TypeScript template. Loads the routing and other common components wrapped by Redux in the ./src/index.tsx
and ./src/app.tsx
files.
Content | Path |
---|---|
Backend code | ./backend/ |
Frontend code | ./frontend/ |
Production build | ./build/ |
Bash scripts | ./scripts/ |
Docker env file | ./.env |
Docker compose files | ./ |
Configuration files | ./ |
Content | Path |
---|---|
Environment variables | ./.env/ |
SSL files | ./.ssl/ |
JWT files | ./.jwt/ |
Public files | ./public/ |
Main logic | ./src/ |
Core functions | ./src/core/ |
Features | ./src/features/ |
Common components | ./src/common/ |
Docker files | ./ |
Configuration files | ./ |
Content | Example |
---|---|
Entry point | ./src/features/feat/index.ts |
Main functions | ./src/features/feat/feat.ts |
Routes | ./src/features/feat/feat.routes.ts |
Database model | ./src/features/feat/feat.model.ts |
Interface | ./src/features/feat/feat.interface.ts |
Integration tests | ./src/features/feat/feat.test.ts |
Swagger documentation | ./src/features/feat/feat.docs.yaml |
Components | ./src/features/feat/components/ |
Email templates | ./src/features/feat/templates/ |
Content | Path |
---|---|
Configuration | ./src/core/config/ |
Server declaration | ./src/core/server/ |
Authentication | ./src/features/auth/ |
Documentation | ./src/features/docs/ |
Home functions | ./src/features/home/ |
Socket functions | ./src/features/socket/ |
User functions | ./src/features/user/ |
Common components | ./src/common/ |
Content | Path |
---|---|
Environment variables | ./.env.* |
Public files | ./public/ |
Main logic | ./src/ |
Core functions | ./src/core/ |
Features | ./src/features/ |
Common components | ./src/common/ |
Images to use in tsx files | ./public/images/ |
(Images to use in scss files) | ./src/images/ |
Docker files | ./ |
Configuration files | ./ |
Content | Example |
---|---|
Entry point | ./src/features/feat/index.ts |
Main functions | ./src/features/feat/feat.tsx |
Interface | ./src/features/feat/feat.interface.ts |
Integration tests | ./src/features/feat/feat.test.tsx |
Redux slice | ./src/features/feat/feat.slice.ts |
Redux slice tests | ./src/features/feat/feat.slice.test.ts |
Components | ./src/features/feat/components/ |
Content | Path |
---|---|
Routing component | ./src/core/routing/ |
Main hooks | ./src/core/hooks/ |
Redux store | ./src/core/store/ |
Color themes | ./src/core/themes/ |
Auth definitions | ./src/features/auth/auth/ |
Activation page | ./src/features/auth/activation/ |
Join page | ./src/features/auth/join/ |
Login page | ./src/features/auth/login/ |
Recovery page | ./src/features/auth/recovery/ |
Reset page | ./src/features/auth/reset/ |
Error page | ./src/features/error/ |
Home page | ./src/features/home/ |
Profile page | ./src/features/user/profile/ |
Background component | ./src/common/background/ |
Kofi component | ./src/common/kofi/ |
Navigation component | ./src/common/navigation/ |
Notification component | ./src/common/notification/ |
Content | Path |
---|---|
Environment variables | ./build/.env/ |
SSL files | ./build/.ssl/ |
JWT files | ./build/.jwt/ |
Backend | ./build/index.js |
Frontend | ./build/frontend/ |
Build logs | ./build/log/ |
Configuration files | ./build/ |
The main routing handler for the backend:
app.use('/', homeRoute)
app.use('/api/auth', authRoute)
app.use('/api/docs', docsRoute)
app.use('*', error.routing)
app.use(error.internal)
Source: ./src/core/server/server.routing.ts
Description | Method | Route |
---|---|---|
Home | GET | / |
Documentation | GET | /api/docs |
Auth check | GET | /api/auth/local/check |
Join | PUT | /api/auth/local/join |
Resend | POST | /api/auth/local/resend |
Activate | POST | /api/auth/local/activate |
Login | POST | /api/auth/local/login |
Logout | GET | /api/auth/local/logout |
Recover | POST | /api/auth/local/recover |
Reset | POST | /api/auth/local/reset |
User profile | POST | /api/user/profile/check |
User remove | POST | /api/user/profile/remove |
The main routing handler for the frontend:
<Route path="/" element={<Home />} />
<Route path="/auth/login" element={<Login />} />
<Route path="/auth/join" element={<Join />} />
<Route path="/auth/activation/:id" element={<Activation />} />
<Route path="/auth/activation/:id/:code" element={<Activation />} />
<Route path="/auth/recovery" element={<Recovery />} />
<Route path="/auth/reset/:id" element={<Reset />} />
<Route path="/auth/reset/:id/:code" element={<Reset />} />
<Route path="/user/profile" element={<Profile />} />
<Route
path="/error/unauthorized"
element={<Error status={401} message="Unauthorized request" />}
/>
<Route
path="/error/notfound"
element={<Error status={404} message="Page not found" />}
/>
<Route path="*" element={<Navigate to="/error/notfound" replace />} />
Source: ./src/core/routing/routing.tsx
Using JWT for authentication and authorization.
JWTs are stored on the frontend in cookies.
Always check the OWASP and make sure your application is as secure as possible.
- Token: 15 minutes (renewing while the refresh token is valid)
- Refresh token: 7 days (strict from login date and time)
Source: ./src/core/config/components/settings.ts
The following image shows the authorization process.
Using global error handlers for routing, internal errors, uncaught exceptions and unhandled rejections.
Source: ./src/core/server/components/error.ts
Custom errors with type, internal error code, and custom message beside the error. The error type helps in sending responses. There are two types currently, 'client' (400 response) and 'server' (500 response).
Type | Code | Message |
---|---|---|
client | 1000 | route not found |
client | 1001 | parameters not found |
client | 1002 | account not found |
client | 1003 | account not activated |
client | 1004 | invalid email or password |
client | 1005 | token not found |
client | 1006 | token error |
client | 1007 | refresh token expired |
client | 1008 | refresh token not found |
Type | Code | Message |
---|---|---|
server | 5000 | database connection error |
server | 5001 | refresh token not saved |
class ClientError extends Error {
type: string
code: number
constructor(code: number, message: string) {
super(message)
this.type = 'client'
this.code = code
}
}
Source: ./src/core/server/server.interface.ts
Example:
throw new ClientError(1000, 'route not found')
class ServerError extends Error {
type: string
code: number
constructor(code: number, message: string) {
super(message)
this.type = 'server'
this.code = code
}
}
Source: ./src/core/server/server.interface.ts
Example:
throw new ServerError(5000, 'database connection error')
Sending responses with a custom function to preserve the standard format.
Source: ./src/core/server/components/response.ts
Status code | Description |
---|---|
200 | Everything worked as expected |
400 | Bad request, often due to missing a required parameter |
401 | Unauthorized, no valid JWT token provided |
404 | Not found, the resource was not found |
500 | Server error, something went wrong on the server |
interface Response {
success: boolean
status: number
result: boolean | object
error: any
}
Source: ./src/core/server/server.interface.ts
Parameters:
res: express.Response,
status: number,
result: boolean | object,
error: any
Example:
return response.send(res, 200, result, false)
The API prepared for websocket connections with Socket.IO.
wss://localhost:3001
To modify or remove it, you can use the files below.
Type | Path |
---|---|
Server | `./src/core/server/server.ts/` |
Configuration | `./src/core/config/socketio.ts` |
Feature | `./src/features/socket/` |
A useful list of commands for sending messages using Socket.IO.
Find the latest version on the official website.
Sending emails using a custom function.
Source: ./src/common/components/mail.ts
interface MailData {
from?: string
to: string
subject: string
content: string
}
Source: ./src/common/common.interface.ts
Parameters:
template: string,
subject: string,
to: string,
data: object
Example:
const templateDir = 'features/auth/auth/templates'
await mail.send(`${templateDir}/join.ejs`, 'Join', result.email, {
user: result,
code: activationCode,
})
The root directory for templates is
src/
, use them relative to that.
The backend uses Mocha for basic integration tests.
The frontend uses React Testing Library for unit/integration tests and Cypress for End-to-End tests.
Focusing on the actual user experience.
Complete E2E testing using Cypress.
Basic unit/integration tests on both ends.
Selecting elements mainly by content both for unit/integration and E2E tests.
If your tests need to be more resilient to change, you can use data-testid
as selector.
Used data-testid to select the dark/light mode buttons and the background component.
If you run all E2E tests, it will create a new user with the following credentials:
Email: [email protected] Password: 12345
At the end of the tests it will remove this user from the database.
Using prettier to keep the code format consistent.
{
"semi": false,
"singleQuote": true
}
Root: ./.prettierrc.json
Add this config file to the backend or frontend folder if need.
Following the Feature/Type/Action scheme, auth/local/login
for example.
Type | Example |
---|---|
Camelcase | mernSeed |
Type | Example |
---|---|
Singular word | user |
camelCase | userOrder |
Type | Example |
---|---|
Singular word | name |
camelCase | lastLogin |
Array as *List | loginList: ['date'] |
It's a full stack project, so I've not included versioning on the backend, but you can also use it as a single API. Just choose between global versioning and evolution designs.
I would suggest to create another instance of the whole backend. Then you could use a reverse proxy to direct the requests to the appropriate version, for example:
v1.example.com/auth/local/check
v2.example.com/auth/local/check
- Create the
v1
andv2
folders insidesrc/
- Copy the content of
src/features/
andsrc/common/
folders into them - Update the imports of the files you just copied
- Create the new routes as
/api/v1/
,/api/v2/
, and keep the default/api/
route for the newest version of the API
Everything stays as it is now, but you have to pay attention to keep a standard scheme and backward compatibility for the responses.
API evolution is the concept of striving to maintain the "I" in API, the request/response body, query parameters, general functionality, etc., only breaking them when you absolutely, absolutely, have to.
More information:
Find it inside the ./package.json
file.
Find it inside the ./backend/package.json
file.
Find it inside the ./frontend/package.json
file.
The MIT License (MIT)
Copyright (c) 2022 Tamas Szoke
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.