Node.JS is a very popular JavaScript runtime that can be used to build web applications in JavaScript. SVR.JS is a web server running on Node.JS that can host both static pages and dynamic content, and can be used to host server-side JavaScript applications.
In this blog post you will build a JavaScript-based web application, and integrate and deploy it with SVR.JS web server.
What is Node.JS?
Node.JS is a powerful, cross-platform, open-source JavaScript runtime environment that is built on the V8 JavaScript engine, which is developed by Google. This innovative technology enables developers to execute JavaScript code on the server side, allowing for the creation of dynamic web applications and services that can handle a wide range of tasks beyond traditional client-side scripting.
One of the key advantages of Node.JS is its ability to run on multiple operating systems, including Windows and various distributions of GNU/Linux. This versatility makes it an attractive choice for developers who want to build applications that can be deployed across different environments without significant modifications.
Node.JS utilizes an event-driven, non-blocking I/O model, which makes it particularly efficient and suitable for handling concurrent connections. This architecture allows for high scalability, enabling applications to manage numerous simultaneous requests with minimal resource consumption. As a result, Node.JS is often used for building real-time applications, such as chat applications, online gaming, and collaborative tools.
Additionally, Node.JS has a rich ecosystem of libraries and frameworks, accessible through npm, which provides developers with a vast array of tools and modules to enhance their applications. This extensive community support and the continuous evolution of the platform contribute to its growing popularity among developers and organizations looking to leverage JavaScript for server-side development.
In summary, Node.JS represents a significant advancement in the world of web development, bridging the gap between client-side and server-side programming, and empowering developers to create fast, scalable, and efficient applications across various platforms.
What is SVR.JS?
SVR.JS is a free, open-source web server built on Node.JS, designed to handle both static and dynamic content efficiently.
It supports server-side JavaScript and PHP applications, making it versatile for various web hosting needs.
Key Features:
- Scalability – utilizes Node.JS's event-driven architecture and supports clustering to manage high request loads effectively.
- Security – includes URL sanitation, protection against brute force attacks on HTTP authentication, and a built-in block list to guard against malicious actors.
- Configurability – allows customization through a
config.json
file and supports the installation of mods to extend server functionality. - Server-side JavaScript – enables the execution of server-side JavaScript, facilitating dynamic web application development.
- Protocol support – offers HTTPS and HTTP/2 support, ensuring secure and efficient communication.
- Compression – provides Brotli, gzip, and Deflate HTTP compression to optimize content delivery.
- Authentication – supports HTTP basic authentication for access control.
- Gateway interfaces – compatible with CGI, SCGI, FastCGI, and PHP, allowing integration with various technologies.
SVR.JS is licensed under the MIT License, ensuring freedom from proprietary software constraints. It is actively maintained, with the latest version being 4.4.0 (as of writing this blog post).
Building the web application
For this blog post, we chose a MERN (MongoDB, Express, React, Node.JS) stack to-do list application. It covers fundamental concepts such as CRUD operations, routing, and basic database interactions. It also demonstrates how to handle user input and update the UI dynamically.
We will build the web application like a regular MERN stack application, and we will integrate it with SVR.JS later on.
Initializing the backend
First, create a directory for your project called "todo-list", and change your working directory to the directory you have just created:
mkdir todo-list
cd todo-list
Then, create the package.json
file in the root of the project with these contents:
{
"name": "todo-list",
"version": "0.0.0",
"private": true
}
After creating the package.json
file, install Express using this command:
npm install express
After installing Express, create a src
directory in the project root, and create two files - app.js
(the Express application), and server.js
(the script invoking the backend server).
The app.js
file will have these contents:
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("Hello World!");
});
module.exports = app;
The server.js
file will have these contents:
const app = require("./app.js");
const port = 3000;
app.listen(port, () => {
console.log(`To-do list application listening on port ${port}`);
});
Run the server using the node src/server.js
command, open your web browser, type http://localhost:3000
in the address bar, and you will get a "Hello World!" text appearing in your browser.
Setting up the development server
During the development of the backend, it is useful to restart the backend server every time the source code is changed. To achieve this, we will use nodemon. First, install nodemon using this command:
npm install nodemon --save-dev
Then create a nodemon.json
file in the project root with these contents:
{
"watch": [
"src/"
]
}
After creating the nodemon configuration, modify the package.json
file to add a dev
script, which starts the backend server, watches the changes in the source code, and restarts the backend server when the source code is changed. The package.json
file after modifications would look like this:
{
"name": "todo-list",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "nodemon src/server.js"
},
"dependencies": {
"express": "^4.21.2"
},
"devDependencies": {
"nodemon": "^3.1.9"
}
}
After modifying the package.json
file, run the development backend server using npm run dev
command. Try changing the source code, like replacing the "Hello World!" message with "To-do list" message in the app.js
file, and the source code will automatically reload.
Setting up the .env
file support
To set up the .env
file support, we will use the dotenv
npm package. To install it, run this command:
npm install dotenv
After installing the package, modify the app.js
and server.js
files inside of the src
directory. The app.js
file will have these contents:
const path = require("path");
require("dotenv").config({
path: path.resolve(__dirname, "..", ".env")
});
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("Hello World!");
});
module.exports = app;
The server.js
file will have these contents:
const app = require("./app.js");
// Environment variables are loaded in the "app.js" file
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`To-do list application listening on port ${port}`);
});
After modifying the scripts, create an .env
file with these contents:
# Port for the web application to listen
PORT=3000
After creating the .env
file, modify the nodemon.json
for nodemon to also watch the .env
file:
{
"watch": [
"src/",
".env"
]
}
After modifying the nodemon configuration, restart the development server for the changes to take effect.
Creating the build script
Right now, we have only the development server with nodemon. But what about the production? That's what the production build scripts come in.
We are going to use SWC to transpile the code for the production. First, install the SWC CLI using this command:
npm install @swc/cli --save-dev
Then, create the SWC configuration file at .swcrc
in the project root with these contents:
{
"jsc": {
"parser": {
"syntax": "ecmascript",
"dynamicImport": true
},
"loose": true,
"target": "es2017"
},
"minify": false,
"module": {
"type": "commonjs"
},
"isModule": false
}
This configuration file will cause SWC to transpile the source code into ES2017 syntax.
Then, install the rimraf
npm package for cleaning up the dist
directory before transpiling the code:
npm install rimraf@v5-legacy --save-dev
Then add the build
(for transpiling the backend code) and start
(for starting the production backend server) scripts. The modified package.json
file will look like this:
{
"name": "todo-list",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "rimraf dist/* && swc -d dist src --strip-leading-paths",
"dev": "nodemon src/server.js",
"start": "node src/server.js"
},
"dependencies": {
"dotenv": "^16.4.7",
"express": "^4.21.2"
},
"devDependencies": {
"@swc/cli": "^0.5.2",
"nodemon": "^3.1.9",
"rimraf": "^5.0.10"
}
}
You can now build the production server using npm run build
command, and run it using the npm run start
command.
Setting up ESLint with Prettier
Setting up ESLint for linting, and Prettier for enforcing the code style is useful for maintaining code quality and readability.
First, install ESLint, Prettier integrations for ESLint, and Prettier using this command:
npm install eslint @eslint/js eslint-config-prettier eslint-plugin-prettier prettier --save-dev
After installing these packages, create the ESLint configuration at eslint.config.js
in the project root with these contents:
const globals = require("globals");
const pluginJs = require("@eslint/js");
const eslintPluginPrettierRecommended = require("eslint-plugin-prettier/recommended");
module.exports = [
{
files: ["src/**/*.js"],
languageOptions: {
sourceType: "commonjs"
}
},
{
languageOptions: {
globals: {
...globals.node
}
}
},
pluginJs.configs.recommended,
eslintPluginPrettierRecommended
];
This configuration integrates Prettier into ESLint and configures linting for all the files in the src
directory.
Then create the Prettier configuration at prettier.config.js
in the project root with these contents:
/**
* @see https://prettier.io/docs/en/configuration.html
* @type {import("prettier").Config}
*/
const config = {
trailingComma: "none",
tabWidth: 2,
semi: true,
singleQuote: false,
endOfLine: "lf"
};
module.exports = config;
After creating the Prettier configuration, add lint
and lint:fix
scripts to the package.json
file. The package.json
file will then look like this:
{
"name": "todo-list",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "rimraf dist/* && swc -d dist src --strip-leading-paths",
"dev": "nodemon src/server.js",
"lint": "eslint --no-error-on-unmatched-pattern src/**/*.js src/*.js",
"lint:fix": "npm run lint -- --fix",
"start": "node dist/server.js"
},
"dependencies": {
"dotenv": "^16.4.7",
"express": "^4.21.2"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@swc/cli": "^0.5.2",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"nodemon": "^3.1.9",
"prettier": "^3.4.2",
"rimraf": "^5.0.10"
}
}
You can now lint the code and enforce the code style using both npm run lint
and npm run lint:fix
scripts.
Initializing the frontend
Now that we have set up the backend, it's now time to set up the frontend. We are going to use Vite for frontend tooling.
First, create a frontend application with Vite using this command:
npm create vite@latest frontend
During creation of the application, you will be asked for various options. Choose these options:
- Select a framework - React
- Select a variant - JavaScript + SWC
Afterwards, change your working directory to the frontend
directory in the project root, and run this command:
npm install
Run the development server using the npm run dev
command, open your web browser, type http://localhost:5173
in the address bar, and you will get this page:
You can optionally configure the frontend for compatiblity with older browsers.
Setting up ESLint with Prettier for the frontend
The default React setup with Vite already includes ESLint out of the box. However, you will integrate Prettier with ESLint.
First, install Prettier integrations for ESLint, and Prettier using this command (run it on the frontend
directory):
npm install @eslint/js eslint-config-prettier eslint-plugin-prettier prettier --save-dev
Then, modify the ESLint configuration at eslint.config.js
file in the frontend
directory to integrate Prettier with it. The end result would look like this:
import js from '@eslint/js';
import globals from 'globals';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import prettierRecommended from 'eslint-plugin-prettier/recommended';
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
settings: { react: { version: '18.3' } },
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
prettierRecommended
];
After modifying the ESLint configuration, create the Prettier configuration at prettier.config.js
in the frontend
directory with these contents:
/**
* @see https://prettier.io/docs/en/configuration.html
* @type {import("prettier").Config}
*/
const config = {
trailingComma: "none",
tabWidth: 2,
semi: true,
singleQuote: false,
endOfLine: "lf"
};
export default config;
After creating the Prettier configuration, add a lint:fix
script to the package.json
file in the frontend
directory. The end result would look like this:
{
"name": "todo-list-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint src/",
"lint:fix": "npm run lint -- --fix",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"prettier": "^3.4.2",
"vite": "^6.0.5"
}
}
You can now lint the code and enforce the code style using both npm run lint
and npm run lint:fix
scripts.
Setting up the frontend proxy to the backend
Setting up a frontend proxy to the backend server is useful when it comes to the frontend development.
To set up a frontend proxy to the backend, modify the vite.config.js
file in the frontend
directory. The end result would look like this (assuming that there was no configuration compatible with older web browsers):
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": {
target: "http://localhost:3000",
changeOrigin: true
}
}
}
});
The Vite development server now proxies /api
routes to the backend server.
Setting up scripts for both frontend and backend in the package.json
file.
Currently, there are two separate private npm packages - one for the backend, and another one for the frontend. However, we would like to build, lint, and start servers using one npm command.
First, install concurrently
using this command (run it in the project root):
npm install concurrently --save-dev
Then, add several scripts to the package.json
file in the project root. The end result would look like this:
{
"name": "todo-list",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "npm run build:backend && npm run build:frontend",
"build:backend": "rimraf dist/* && swc -d dist src --strip-leading-paths",
"build:frontend": "cd frontend && npm run build",
"dev": "concurrently \"nodemon src/server.js\" \"cd frontend && npm run dev\"",
"lint": "npm run lint:backend && npm run lint:frontend",
"lint:fix": "npm run lint:backend-fix && npm run lint:frontend-fix",
"lint:backend": "eslint --no-error-on-unmatched-pattern src/**/*.js src/*.js",
"lint:backend-fix": "npm run lint:backend -- --fix",
"lint:frontend": "cd frontend && npm run lint",
"lint:frontend-fix": "cd frontend && npm run lint:fix",
"postinstall": "cd frontend && npm install",
"start": "node dist/server.js"
},
"dependencies": {
"dotenv": "^16.4.7",
"express": "^4.21.2"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@swc/cli": "^0.5.2",
"concurrently": "^9.1.2",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"nodemon": "^3.1.9",
"prettier": "^3.4.2",
"rimraf": "^5.0.10"
}
}
You can now run the development server for both the frontend and the backend using the npm run dev
command, and lint and enforce the code style using the npm run lint
and npm run lint:fix
command.
Connecting the backend to the database
First, install MongoDB on your development environment according to its documentation, and start the MongoDB service.
For connecting the backend with the database, we are going to use Mongoose ODM. It allows to define a model with a schema for a MongoDB collection.
To install Mongoose, run this command in the project root:
npm install mongoose
After installing Mongoose, modify the .env
file in the project root to include the MongoDB connection string. The end result would look like this:
# Port for the web application to listen
PORT=3000
# MongoDB connection string
MONGODB_CONNSTRING=mongodb://localhost:27017/todo
After adding the MongoDB connection string to the .env
file, add code that connects to the database to the app.js
file. The end result would look like this:
const path = require("path");
require("dotenv").config({
path: path.resolve(__dirname, "..", ".env")
});
const mongoose = require("mongoose");
const express = require("express");
mongoose
.connect(process.env.MONGODB_CONNSTRING)
.then(() => console.log(`Database connected successfully`));
// Since mongoose's Promise is deprecated, we override it with Node's Promise
mongoose.Promise = global.Promise;
const app = express();
app.get("/", (req, res) => {
res.send("Hello World!");
});
module.exports = app;
The backend will now connect to a MongoDB database.
Developing the backend
First, create a models
directory in the src
directory in the project root. After creating this directory, create a task model at task.js
in the newly created models
directory with these contents:
const mongoose = require("mongoose");
const taskSchema = new mongoose.Schema({
description: {
type: String,
required: true
},
completed: {
type: Boolean,
required: true
}
});
const Task = mongoose.model("tasks", taskSchema);
module.exports = Task;
This model has two fields - description
containing the task description, and completed
containing the state of whenever the task is completed or not.
After creating a model, create a routes
directory in the src
directory in the project root. After creating this directory, create a task route at task.js
in the newly created src
directory with these contents:
const Task = require("../models/task.js");
const express = require("express");
const router = express.Router();
router.get("/", (req, res) => {
Task.find()
.then((result) => {
res.json({
tasks: result.map((task) => ({
id: task.id,
description: task.description,
completed: task.completed
}))
});
})
.catch((err) => {
res.status(500).json({ message: err.message });
});
});
router.post("/", (req, res) => {
if (!req.body || !req.body.description) {
res.status(400).json({ message: "You need to provide a task description" });
return;
}
Task.create({
description: req.body.description,
completed: false
})
.then(() => {
res.json({ message: "Task added successfully" });
})
.catch((err) => {
res.status(500).json({ message: err.message });
});
});
router.get("/:id", (req, res) => {
Task.findOne({ _id: req.params.id })
.then((result) => {
if (!result) {
res.status(404).json({ message: "The task doesn't exist" });
return;
}
res.json({
id: result.id,
description: result.description,
completed: result.completed
});
})
.catch((err) => {
res.status(500).json({ message: err.message });
});
});
router.patch("/:id", (req, res) => {
if (!req.body || (!req.body.description && req.body.completed !== false && !req.body.completed)) {
res.status(400).json({
message:
"You need to provide either a task description or the state of the task"
});
return;
}
Task.findOne({ _id: req.params.id })
.then((result) => {
if (!result) {
res.status(404).json({ message: "The task doesn't exist" });
return;
}
Task.updateOne(
{ _id: req.params.id },
{
description: req.body.description,
completed: req.body.completed
}
)
.then(() => {
res.json({ message: "Task updated successfully" });
})
.catch((err) => {
res.status(500).json({ message: err.message });
});
})
.catch((err) => {
res.status(500).json({ message: err.message });
});
});
router.delete("/:id", (req, res) => {
Task.findOne({ _id: req.params.id })
.then((result) => {
if (!result) {
res.status(404).json({ message: "The task doesn't exist" });
return;
}
Task.deleteOne({ _id: req.params.id })
.then(() => {
res.json({ message: "Task deleted successfully" });
})
.catch((err) => {
res.status(500).json({ message: err.message });
});
})
.catch((err) => {
res.status(500).json({ message: err.message });
});
});
module.exports = router;
After creating the route, modify the app.js
directory in the src
directory in the project root to include the route you have just created. The end result would look like this:
const path = require("path");
require("dotenv").config({
path: path.resolve(__dirname, "..", ".env")
});
const mongoose = require("mongoose");
const express = require("express");
const bodyParser = require("body-parser");
const taskRoute = require("./routes/task.js");
mongoose
.connect(process.env.MONGODB_CONNSTRING)
.then(() => console.log(`Database connected successfully`));
// Since mongoose's Promise is deprecated, we override it with Node's Promise
mongoose.Promise = global.Promise;
const app = express();
app.use("/api", bodyParser.json());
app.use("/api/task", taskRoute);
module.exports = app;
The backend server now exposes these API endpoints for the to-do list:
- GET
/api/task
- obtains the list of all tasks - POST
/api/task
- creates a new task - GET
/api/task/{id}
- obtains a specific task - PATCH
/api/task/{id}
- changes the properties of a specific task - DELETE
/api/task/{id}
- deletes a specific task
For additional security, you can disable the X-Powered-By
header in Express. To do this, modify the app.js
file in the src
directory. The end result would look like this:
const path = require("path");
require("dotenv").config({
path: path.resolve(__dirname, "..", ".env")
});
const mongoose = require("mongoose");
const express = require("express");
const bodyParser = require("body-parser");
const taskRoute = require("./routes/task.js");
mongoose
.connect(process.env.MONGODB_CONNSTRING)
.then(() => console.log(`Database connected successfully`));
// Since mongoose's Promise is deprecated, we override it with Node's Promise
mongoose.Promise = global.Promise;
const app = express();
app.disable("x-powered-by");
app.use("/api", bodyParser.json());
app.use("/api/task", taskRoute);
module.exports = app;
Also, you can disable caching for the API endpoints, since we will set the caching headers for both web browser and SVR.JS Cache mod to cache the response. To do this, you can use the nocache
npm package, which can be used with Express. To use the package, install it with this command (run it on the project root):
npm install nocache
After installing the package, modify the app.js
file in the src
directory to use the nocache
middleware for the API endpoints. The end result would look like this:
const path = require("path");
require("dotenv").config({
path: path.resolve(__dirname, "..", ".env")
});
const mongoose = require("mongoose");
const express = require("express");
const bodyParser = require("body-parser");
const noCache = require("nocache");
const taskRoute = require("./routes/task.js");
mongoose
.connect(process.env.MONGODB_CONNSTRING)
.then(() => console.log(`Database connected successfully`));
// Since mongoose's Promise is deprecated, we override it with Node's Promise
mongoose.Promise = global.Promise;
const app = express();
app.disable("x-powered-by");
app.use("/api", noCache());
app.use("/api", bodyParser.json());
app.use("/api/task", taskRoute);
module.exports = app;
Developing the frontend
First, open the index.html
file inside the frontend
directory. You will see something like this:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
You can change the title of the web application, change the favicon (add it to the public
directory in the frontend
directory), and add a message in case JavaScript support is not present.
For example, you can modify the index.html
file to have these contents:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>To-do list</title>
</head>
<body>
<noscript>This application requires JavaScript to work correctly.</noscript>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
After modifying the index.html
file, you will see that the title in the browser has been changed, if the development server has been started.
For styling, we are going to use Bootstrap and React Bootstrap. Bootstrap is a popular open-source front-end framework used for developing responsive and mobile-first websites. React Bootstrap is a re-implementation of Bootstrap JavaScript with React components.
To install both Bootstrap and React Bootstrap, run this command on the frontend
directory:
npm install bootstrap react-bootstrap
Then, include Bootstrap's CSS file in the index.jsx
file and remove default index.css
include, since we are not going to customize Bootstrap. The end result would look like this:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "bootstrap/dist/css/bootstrap.min.css";
import App from "./App.jsx";
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>
);
Then, replace the contents of App.jsx
file in the src
directory in the frontend
directory with this:
import { useEffect, useRef, useState } from "react";
import Button from "react-bootstrap/Button";
import Form from "react-bootstrap/Form";
import InputGroup from "react-bootstrap/InputGroup";
import ListGroup from "react-bootstrap/ListGroup";
import Spinner from "react-bootstrap/Spinner";
import Alert from "react-bootstrap/Alert";
function App() {
const [loading, setLoading] = useState(true);
const [tasks, setTasks] = useState({});
const [error, setError] = useState(null);
const [refresh, setRefresh] = useState(true);
const taskInputRef = useRef();
useEffect(() => {
const getTasks = async () => {
try {
const res = await fetch("/api/task", { method: "GET" });
const data = await res.json();
if (res.ok) {
setTasks(data.tasks);
} else {
setError(data.message);
}
} catch (err) {
setError(err.message);
}
setLoading(false);
setRefresh(false);
};
if (refresh) {
getTasks();
} else {
const interval = setInterval(getTasks, 5000);
return () => clearInterval(interval);
}
}, [refresh]);
if (loading) {
return (
<main className="d-flex flex-row min-vh-100">
<div className="text-center align-self-center w-100">
<Spinner
animation="border"
className="align-self-center d-inline-block"
role="status"
>
<span className="visually-hidden">Loading...</span>
</Spinner>
</div>
</main>
);
} else if (error) {
return (
<main className="container py-5">
<h1>Error</h1>
<Alert variant="danger">An unexpected error occurred: {error}</Alert>
</main>
);
} else {
return (
<main className="container py-5">
<h1>To-do list</h1>
<form
className="mb-4"
onSubmit={async (e) => {
e.preventDefault();
if (taskInputRef.current.value) {
try {
const res = await fetch(`/api/task`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
description: taskInputRef.current.value
})
});
if (res.ok) {
taskInputRef.current.value = "";
setRefresh(true);
}
// eslint-disable-next-line no-unused-vars
} catch (err) {
// Can't add task
}
}
}}
>
<InputGroup>
<Form.Control
placeholder="Task to add..."
aria-label="Task to add..."
ref={taskInputRef}
/>
<Button variant="primary" type="submit">
Add
</Button>
</InputGroup>
</form>
<ListGroup>
{tasks.map((task) => (
<ListGroup.Item className="d-flex flex-row" key={task.id}>
<Form.Check
className="flex-grow-1 align-self-center mb-0 text-truncate ps-1"
type="checkbox"
id={`task-${task.id}`}
>
<Form.Check.Input
type="checkbox"
checked={task.completed}
onChange={async (e) => {
e.preventDefault();
try {
const res = await fetch(
`/api/task/${encodeURI(
task.id
.replace(/(?:^|[/\\])\.\.(?=[/\\])/g, "")
.replace(/^[/\\]+/g, "")
.replace(/[/\\]+$/g, "")
)}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ completed: !task.completed })
}
);
if (res.ok) {
setRefresh(true);
}
// eslint-disable-next-line no-unused-vars
} catch (err) {
// Can't toggle task
}
}}
className="ms-0 me-2"
/>
<Form.Check.Label
className={
task.completed
? "text-decoration-line-through text-muted d-inline"
: ""
}
title={task.description}
>
{task.description}
</Form.Check.Label>
</Form.Check>
<Button
className="align-self-center flex-shrink-0"
size="sm"
variant="danger"
onClick={async (e) => {
e.preventDefault();
try {
const res = await fetch(
`/api/task/${encodeURI(
task.id
.replace(/(?:^|[/\\])\.\.(?=[/\\])/g, "")
.replace(/^[/\\]+/g, "")
.replace(/[/\\]+$/g, "")
)}`,
{
method: "DELETE"
}
);
if (res.ok) {
setRefresh(true);
}
// eslint-disable-next-line no-unused-vars
} catch (err) {
// Can't delete task
}
}}
>
Delete
</Button>
</ListGroup.Item>
))}
</ListGroup>
</main>
);
}
}
export default App;
The App.jsx
file contains a React functional component that implements a simple to-do list application. It utilizes several hooks from React, such as useState
, useEffect
, and useRef
, along with components from the React Bootstrap library for styling and layout. The application allows users to view, add, toggle, and delete tasks, with asynchronous interactions to a backend API for data management.
At the beginning of the component, several state variables are defined using the useState
hook. loading
indicates whether the application is currently fetching data, tasks
holds the list of tasks retrieved from the server, error
captures any error messages that may occur during data fetching or manipulation, and refresh
is a flag that triggers data refresh. The taskInputRef
is created using useRef
to reference the input field for adding new tasks. The useEffect
hook is employed to fetch tasks from the server when the component mounts or when the refresh
state changes. The getTasks
function is defined as an asynchronous function that makes a GET request to the /api/task
endpoint. If the request is successful, it updates the tasks
state; otherwise, it sets the error
state with the error message. The loading state is set to false after the data fetching is complete
The component's rendering logic is structured to handle three main states: loading, error, and the main application view. If loading
is true, a spinner is displayed to indicate that data is being fetched. If an error occurs, an alert is shown with the error message. If the data is successfully loaded, the main application view is rendered, which includes a form for adding new tasks and a list of existing tasks. The form captures user input, checks if task is not empty and, upon submission, sends a POST request to the /api/task
endpoint to create a new task. If the request is successful, the input field is cleared, and the refresh
state is set to true to reload the task list.
The list of tasks is displayed using the ListGroup
component from React Bootstrap. Each task is represented as a ListGroup.Item
, which includes a checkbox for toggling the task's completion status and a delete button. The checkbox is controlled by the completed
property of each task, and changing its state triggers a PATCH request to update the task on the server. The delete button sends a DELETE request to remove the task from the server. Both the toggle and delete operations set the refresh
state to true upon successful completion, prompting the application to fetch the updated task list.
After modifying the App.jsx
file, you can safely delete these files and directories from the frontend:
frontend/src/App.css
frontend/src/index.css
frontend/src/assets/react.svg
frontend/src/assets
If you start a development server, and type http://localhost:5173
in the address bar, you will get a page, which looks like this:
Serving the frontend from the backend
The backend server right now doesn't serve the frontend assets, just API endpoints, so we are going to add static file serving functionality with express.static
, which is built-in Express middleware for static file serving, and a wrapper over serve-static
library.
To include static file serving, modify the app.js
directory in the src
directory in the project root. The end result would look like this:
const path = require("path");
require("dotenv").config({
path: path.resolve(__dirname, "..", ".env")
});
const mongoose = require("mongoose");
const express = require("express");
const bodyParser = require("body-parser");
const noCache = require("nocache");
const taskRoute = require("./routes/task.js");
mongoose
.connect(process.env.MONGODB_CONNSTRING)
.then(() => console.log(`Database connected successfully`));
// Since mongoose's Promise is deprecated, we override it with Node's Promise
mongoose.Promise = global.Promise;
const app = express();
app.disable("x-powered-by");
app.use("/api", noCache());
app.use("/api", bodyParser.json());
app.use("/api/task", taskRoute);
app.use(express.static(path.join(__dirname, "..", "frontend", "dist")));
module.exports = app;
Now you can run npm run build
in the project root, then npm run start
to start the production server. Open your web browser, type http://localhost:3000
in the address bar, and you will get the frontend for the to-do list.
Optional: set up Git
If you want to set up Git for the web application, first create a .gitignore
file in the project root with these contents:
# Dependencies
node_modules/
# Build directories
/dist/
# Environment variables
.env
# Editor directories and files
*.swa-p
The .gitignore
file tells Git to ignore the node_modules
directory, dist
directories, .env
files, and editor directories and files.
After creating the .gitignore
file, create a .env.example
file with these contents:
# Port for the web application to listen
PORT=3000
# MongoDB connection string
MONGODB_CONNSTRING=
After creating the .env.example
file, initialize and create an initial commit using these commands:
git init .
git add .
git commit -m 'chore: init'
Integrating the web application with SVR.JS
Now that you have built the web application, we are going to integrate it with SVR.JS web server, and later on deploy the web application to production environment along with SVR.JS. To do that, we can develop a custom SVR.JS mod.
To create a SVR.JS mod, first clone the Git repository of SVR.JS mods starter (outside of the project root for the web application), rename the directory containing the contents of the repository, and change your working directory to the directory you just renamed:
git clone https://git.svrjs.org/svrjs/svrjs-mod-starter.git
mv svrjs-mod-starter todo-list-svrjs-mod
cd todo-list-svrjs-mod
After initializing the SVR.JS mod project, delete the .git
directory inside of the SVR.JS mod project root, because the previous Git commit history for the SVR.JS mod starter is not needed:
rm -r .git
You can also delete the default LICENSE
file, since the SVR.JS mod may have a different license:
rm LICENSE
The SVR.JS mod starter you have just cloned already includes ESLint integrated with Prettier, so you don't need to do additional configuration for linting and enforcing the code style. The SVR.JS mod starter also includes Jest for testing, although we are not going to write tests for the SVR.JS mod.
Before building a SVR.JS mod, install dependencies using this command:
npm install
Change the mod information in the modInfo.json
file. If you open the file, it may look like this:
{
"name": "Example mod",
"version": "0.0.0"
}
Change the mod information to describe what the mod does, for example like this:
{
"name": "To-do list integration for SVR.JS",
"version": "0.0.0"
}
After changing the mod information, modify the index.js
file in the src
directory for the SVR.JS mod project root to integrate the to-do list application you have just developed with SVR.JS web server. The SVR.JS mod will call the web applications callback created with Express. The end result would look like this:
const modInfo = require("../modInfo.json"); // SVR.JS mod information
let app = null;
let appImportError = null;
try {
if (!process.serverConfig.todoListBackendRoot) {
throw new Error("The to-do list backend root is not specified.");
}
app = require(process.serverConfig.todoListBackendRoot + "/dist/app.js");
} catch (err) {
appImportError = err;
}
// Exported SVR.JS mod callback
// eslint-disable-next-line no-unused-vars
module.exports = (req, res, logFacilities, config, next) => {
if (appImportError) {
res.error(500, `todo-list-svrjs-mod/${modInfo.version}`, appImportError);
return;
}
app(req, res);
};
// SVR.JS configuration property validators
module.exports.configValidators = {
todoListBackendRoot: (value) => typeof value === "string"
};
module.exports.modInfo = modInfo;
After modifying the index.js
file, it's now safe to delete these files and directories:
src/utils
tests/utils
(we are not going to write tests for this SVR.JS mod)
Build the mod using this command:
npm run build
Then follow the instructions in the README file to test the mod. The following instruction are from the README file in the SVR.JS mod starter:
To test the mod:
1. Clone the SVR.JS repository with "git clone https://git.svrjs.org/svrjs/svrjs.git" command.
2. Change the working directory to "svrjs" using "cd svrjs".
3. Build SVR.JS by first running "npm install" and then running "npm run build".
4. Copy the mod into mods directory in the dist directory using "cp ../dist/mod.js dist/mods" (GNU/Linux, Unix, BSD) or "copy ..\dist\mod.js dist\mods" (Windows).
5. Do the necessary mod configuration if the mod requires it.
6. Run SVR.JS by running "npm start".
7. Do some requests to the endpoints covered by the mod.
After following the instructions in the README file (excluding modifying SVR.JS's config.json
file), you will have started SVR.JS web server. If you however try accessing the website after typing http://localhost/
in the address bar in your browser, you will face a 500 Internal Server Error message, which looks like this:
This is because we didn't configure the backend root for the to-do list. To do that, open the config.json
file in the dist
directory inside the svrjs
directory to add the backend root. The end result would look like this (replace /path/to/todo-list
with actual path to the web application project root):
{
"users": [],
"port": 80,
"pubport": 80,
"page404": "404.html",
"timestamp": 1735676984978,
"blacklist": [],
"nonStandardCodes": [],
"enableCompression": true,
"customHeaders": {},
"enableHTTP2": false,
"enableLogging": true,
"enableDirectoryListing": true,
"enableDirectoryListingWithDefaultHead": false,
"serverAdministratorEmail": "[no contact information]",
"stackHidden": false,
"enableRemoteLogBrowsing": false,
"exposeServerVersion": true,
"disableServerSideScriptExpose": true,
"rewriteMap": [],
"allowStatus": true,
"dontCompress": [
"/.*\\.ipxe$/",
"/.*\\.(?:jpe?g|png|bmp|tiff|jfif|gif|webp)$/",
"/.*\\.(?:[id]mg|iso|flp)$/",
"/.*\\.(?:zip|rar|bz2|[gb7x]z|lzma|tar)$/",
"/.*\\.(?:mp[34]|mov|wm[av]|avi|webm|og[gv]|mk[va])$/"
],
"enableIPSpoofing": false,
"secure": false,
"sni": {},
"disableNonEncryptedServer": false,
"disableToHTTPSRedirect": false,
"enableETag": true,
"disableUnusedWorkerTermination": false,
"rewriteDirtyURLs": true,
"errorPages": [],
"useWebRootServerSideScript": true,
"exposeModsInErrorPages": true,
"disableTrailingSlashRedirects": false,
"environmentVariables": {},
"allowDoubleSlashes": false,
"optOutOfStatisticsServer": false,
"disableConfigurationSaving": false,
"enableIncludingHeadAndFootInHTML": true,
"todoListBackendRoot": "/path/to/todo-list"
}
After changing the SVR.JS configuration, restart SVR.JS, refresh the page in your web browser, and you will have a to-do list.
However, static files are served via express.static
middleware, which may be slower with SVR.JS than using SVR.JS's built-in static file serving functionality. To fix this, modify the index.js
file in the src
directory for the SVR.JS mod project root to load the web application handler only for API endpoints, and handle all other requests with SVR.JS's static file serving functionality. The end result would look like this:
const modInfo = require("../modInfo.json"); // SVR.JS mod information
let app = null;
let appImportError = null;
try {
if (!process.serverConfig.todoListBackendRoot) {
throw new Error("The to-do list backend root is not specified.");
}
app = require(process.serverConfig.todoListBackendRoot + "/dist/app.js");
} catch (err) {
appImportError = err;
}
// Exported SVR.JS mod callback
module.exports = (req, res, logFacilities, config, next) => {
if (appImportError) {
res.error(500, `todo-list-svrjs-mod/${modInfo.version}`, appImportError);
return;
}
if (req.parsedURL.pathname.match(/^\/api(?:$|[?#/])/)) {
app(req, res);
} else {
next();
}
};
// SVR.JS configuration property validators
module.exports.configValidators = {
todoListBackendRoot: (value) => typeof value === "string"
};
module.exports.modInfo = modInfo;
After modifying the index.js
file, run these commands on SVR.JS mod project root:
npm run build
cd svrjs
cp ../dist/mod.js dist/mods
After running the commands, modify the SVR.JS configuration (config.json
file in the dist
directory inside the svrjs
directory) to add the frontend's dist
directory as a webroot. The end result would look like this (replace /path/to/todo-list
with actual path to the web application project root):
{
"users": [],
"port": 80,
"pubport": 80,
"page404": "404.html",
"timestamp": 1735676984978,
"blacklist": [],
"nonStandardCodes": [],
"enableCompression": true,
"customHeaders": {},
"enableHTTP2": false,
"enableLogging": true,
"enableDirectoryListing": true,
"enableDirectoryListingWithDefaultHead": false,
"serverAdministratorEmail": "[no contact information]",
"stackHidden": false,
"enableRemoteLogBrowsing": false,
"exposeServerVersion": true,
"disableServerSideScriptExpose": true,
"rewriteMap": [],
"allowStatus": true,
"dontCompress": [
"/.*\\.ipxe$/",
"/.*\\.(?:jpe?g|png|bmp|tiff|jfif|gif|webp)$/",
"/.*\\.(?:[id]mg|iso|flp)$/",
"/.*\\.(?:zip|rar|bz2|[gb7x]z|lzma|tar)$/",
"/.*\\.(?:mp[34]|mov|wm[av]|avi|webm|og[gv]|mk[va])$/"
],
"enableIPSpoofing": false,
"secure": false,
"sni": {},
"disableNonEncryptedServer": false,
"disableToHTTPSRedirect": false,
"enableETag": true,
"disableUnusedWorkerTermination": false,
"rewriteDirtyURLs": true,
"errorPages": [],
"useWebRootServerSideScript": true,
"exposeModsInErrorPages": true,
"disableTrailingSlashRedirects": false,
"environmentVariables": {},
"allowDoubleSlashes": false,
"optOutOfStatisticsServer": false,
"disableConfigurationSaving": false,
"enableIncludingHeadAndFootInHTML": true,
"todoListBackendRoot": "/path/to/todo-list",
"wwwroot": "/path/to/todo-list/frontend/dist"
}
After modifying the SVR.JS configuration, run the npm run start
command in the svrjs
directory to start the SVR.JS web server.
Refresh the page in your web browser again, and you will still see the to-do list application's frontend, this time served with SVR.JS's static file serving functionality instead of the express.static
middleware.
You have now created a SVR.JS mod integrating your web application with SVR.JS web server.
Deploying the web application to the production environment
Now that you have built a web application, and integrated it with SVR.JS web server, it's now time to deploy it to the production environment.
For this post, we assume you have a server running a Debian-based GNU/Linux distribution (like Debian, Ubuntu Server, Devuan) with SSH access.
Connecting to the server
First, obtain the IP address and the credentials for the server, and log into the server through SSH:
ssh sysadmin@10.0.0.2 # Replace "sysadmin" with the obtained username, and "10.0.0.2" with the obtained IP address or hostname.
If you log into the server for the first time, you may have been asked about the authenticity of the host. Check the key footprint, and if it is correct, type yes
to continue connecting.
After confirming the authenticity of the host, type in your password, and you will now have access to the server.
Installing firewall
We are going to protect the server using UFW (Uncomplicated Firewall), which is firewall software utilizing netfilter
and designed to be easy to use. We are going to use a firewall to restrict access to some services from the public. After connecting to the server, install the firewall using this command:
sudo apt update
sudo apt install ufw
After installing the firewall, allow HTTP, HTTPS and SSH traffic, and enable it:
sudo ufw allow http
sudo ufw allow https
sudo ufw allow ssh
sudo ufw enable
Installing Node.JS, npm and SVR.JS
After enabling the firewall, install curl, Node.JS and npm using this command:
sudo apt install curl nodejs npm
We are going to use npm to install dependencies for the web application we are going to deploy.
Open your web browser, type https://svrjs.org
in the address bar, and you will get SVR.JS website, from which you will copy the installation command. Click on the Linux icon, and above it click on the copy icon to copy the installation command. The installation command would look like this:
sudo bash -c "$(curl -fsSL https://downloads.svrjs.org/installer/svr.js.installer.linux.20250105.sh)"
During the installation, you will be asked for some options. Type 0
to install a stable SVR.JS version, since we are going to use SVR.JS mods that work only on SVR.JS 4.0.0 or later. After selecting the SVR.JS version, you may be also asked whenever to install dependencies. Type y
at every dependency installation prompt to allow installing the dependencies.
If you open a web browser and type in http://10.0.0.2/
(replace 10.0.0.2
with your server's IP address or hostname), you will get the default SVR.JS index page, which looks like this:
Installing MongoDB
Install MongoDB on your server according to its documentation, and start the MongoDB service using either sudo /etc/init.d/mongod start
(if you use systemd) or sudo systemctl mongod start
(if you don't use systemd) command.
Configuring MongoDB
We are going to enable access control in MongoDB to prevent unauthorized database access.
First, open the mongo
shell by running the sudo mongo
command.
After opening the mongo
shell, run this script (paste it into the mongo
shell):
use admin
db.createUser(
{
user: "admin", // Replace "admin" with your desired username for database administration
pwd: passwordPrompt(),
roles: [ { role: "userAdminAnyDatabase", db: "admin" }, "readWriteAnyDatabase" ]
}
)
You may have been asked about the administration password. Type in your password used for MongoDB database administration.
After creating an administrator user, create a user used by the web application. Run this script (paste it into the mongo shell):
use todo
db.createUser(
{
user: "todouser", // Replace "todouser" with your desired username used by the web application
pwd: "todopass", // Replace "todopass" with your desired password used by the web application
roles: [ { role: "readWrite", db: "todo" } ]
}
)
After creating an user used by the web application, modify the MongoDB configuration at /etc/mongod.conf
using your text-mode editor (like nano
) to enable access control. Add these lines to the end of the configuration file:
security:
authorization: enabled
After enabling access control, restart the MongoDB service using either sudo /etc/init.d/mongod restart
(if you use systemd) or sudo systemctl mongod restart
(if you don't use systemd) command.
You now have a MongoDB service with access control enabled.
Configuring SVR.JS and web application
First, obtain the SSL/TLS certificate, since we are going to enable HTTPS. You can use Certbot to obtain the certificate from Let's Encrypt for free.
Once you have obtained the SSL/TLS certificate, copy the certificate and the private key to /usr/lib/svrjs/cert/cert.crt
and /usr/lib/svrjs/cert/key.key
respectively, and change the ownership of these files to the svrjs
user to make these files accessible to SVR.JS:
sudo mkdir /usr/lib/svrjs/cert
sudo cp /path/to/cert.crt /usr/lib/svrjs/cert/cert.crt # Replace "/path/to/cert.crt" with an actual path to the SSL/TLS certificate
sudo cp /path/to/key.key /usr/lib/svrjs/cert/key.key # Replace "/path/to/key.key" with an actual path to the private key
sudo chown -hR svrjs:svrjs /usr/lib/svrjs/cert
Then copy the web application files and the SVR.JS mod that integrates SVR.JS with the web application, to the server. First, open a new terminal tab or window, and run these commands (on the client) to create an archive and send it to the server (assuming your computer runs GNU/Linux):
sudo tar -czf todo.tar.gz /path/to/todo-list # Replace "/path/to/todo-list" with actual path to a web application project root
scp todo.tar.gz sysadmin@10.0.0.2:/tmp # Replace "sysadmin" with the obtained username, and "10.0.0.2" with the obtained IP address or hostname.
cd /path/to/todo-list-svrjs-mod # Replace "/path/to/todo-list-svrjs-mod" with actual path to a web application integration mod project root
npm run build # Build the mod
scp dist/mod.js sysadmin@10.0.0.2:/tmp # Replace "sysadmin" with the obtained username, and "10.0.0.2" with the obtained IP address or hostname.
Type in your password used to connect to your server through SSH.
Then switch back to a terminal window or tab with an active SSH session, and run these commands on the server to extract the web application files:
cd /usr/lib
sudo tar -xzf /tmp/todo.tar.gz
Then perform a clean install on both the backend and the frontend of the web application by running these commands:
cd /usr/lib/todo-list
sudo npm ci
cd frontend
sudo npm ci
Then modify the /usr/lib/todo-list/.env
file in a text-based editor to change the MongoDB connection string to include credentials and the database name. The end result would look like this:
# Port for the web application to listen
PORT=3000
# MongoDB connection string
# Replace "todouser" with MongoDB username and "todopass" with MongoDB password.
MONGODB_CONNSTRING=mongodb://todouser:todopass@localhost:27017/todo
After modifying the .env
file, grant the ownership of the /usr/lib/todo-list
directory and files and subdirectories to the svrjs
user with this command:
sudo chown -hR svrjs:svrjs /usr/lib/todo-list
Afterwards, install both the integration mod you have developed and SVR.JS Cache mod for caching the responses to speed up the website:
cd /usr/lib/svrjs/mods
sudo cp /tmp/mod.js 01-todo-list.js
sudo wget https://downloads.svrjs.org/mods/svrjs-cache-mod.1.1.0.js # Replace "1.1.0" with the latest version of SVR.JS Cache mod, which can be found at https://svrjs.org/mods.
sudo mv svrjs-cache-mod.1.1.0.js 00-svrjs-cache-mod.1.1.0.js # Replace "1.1.0" with the latest version of SVR.JS Cache mod, which can be found at https://svrjs.org/mods.
After installing mods, change the SVR.JS configuration at /etc/svrjs-config.json
in a text-based editor to set caching headers, add security headers, configure caching, configure HTTP authentication and configure the integration mod you have developed. The end result would look like this:
{
"users": [],
"port": 80,
"pubport": 80,
"sport": 443,
"spubport": 443,
"page404": "404.html",
"timestamp": 1735716562484,
"blacklist": [],
"nonStandardCodes": [
{
"scode": 401,
"realm": "To-do list access",
"regex": "/^.+$/"
}
],
"enableCompression": true,
"customHeaders": {
"Cache-Control": "public, max-age=300",
"x-content-type-options": "nosniff",
"Content-Security-Policy": "default-src 'self'; script-src 'self' 'sha256-VA8O2hAdooB288EpSTrGLl7z3QikbWU9wwoebO/QaYk=' 'sha256-+5XkZFazzJo8n0iOP4ti/cLCMUudTf//Mzkb7xNPXIc=' 'sha256-MS6/3FCg4WjP9gwgaBGwLpRCY6fZBgwmhVCdrPrNf3E=' 'sha256-tQjf8gvb2ROOMapIxFvFAYBeUJ0v1HCbOcSmDNXGtDo='; style-src 'self' 'unsafe-inline'; img-src 'self' data:;",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "geolocation=(), camera=(), microphone=(), fullscreen=*",
"Feature-Policy": "geolocation 'none', camera 'none', microphone 'none', fullscreen *"
},
"enableHTTP2": false,
"enableLogging": true,
"enableDirectoryListing": false,
"enableDirectoryListingWithDefaultHead": false,
"serverAdministratorEmail": "[no contact information]",
"stackHidden": true,
"enableRemoteLogBrowsing": false,
"exposeServerVersion": false,
"disableServerSideScriptExpose": true,
"rewriteMap": [],
"allowStatus": false,
"dontCompress": [
"/.*\\.ipxe$/",
"/.*\\.(?:jpe?g|png|bmp|tiff|jfif|gif|webp)$/",
"/.*\\.(?:[id]mg|iso|flp)$/",
"/.*\\.(?:zip|rar|bz2|[gb7x]z|lzma|tar)$/",
"/.*\\.(?:mp[34]|mov|wm[av]|avi|webm|og[gv]|mk[va])$/"
],
"enableIPSpoofing": false,
"secure": true,
"cert": "cert/cert.crt",
"key": "cert/key.key",
"sni": {},
"disableNonEncryptedServer": false,
"disableToHTTPSRedirect": false,
"enableETag": true,
"disableUnusedWorkerTermination": false,
"rewriteDirtyURLs": true,
"errorPages": [],
"useWebRootServerSideScript": false,
"exposeModsInErrorPages": false,
"disableTrailingSlashRedirects": false,
"environmentVariables": {},
"allowDoubleSlashes": false,
"optOutOfStatisticsServer": false,
"disableConfigurationSaving": false,
"enableIncludingHeadAndFootInHTML": false,
"todoListBackendRoot": "/usr/lib/todo-list",
"wwwroot": "/usr/lib/todo-list/frontend/dist",
"cacheVaryHeaders": [
"ETag",
"Accept-Encoding"
]
}
We are going to use HTTP authentication to prevent unauthorized users from accessing the web application.
After modifying SVR.JS configuration, create a user in SVR.JS using this command:
sudo svrpasswd -a user # Replace "user" with username you will use to log into the web application.
During the user creation, you will be asked for the password hashing algorithm. Type scrypt
to use scrypt
password hashing algorithm. After choosing the password hashing algorithm, type in the password you will use to access the web application two times.
After creating the user, restart SVR.JS using either sudo /etc/init.d/svrjs restart
(if you use systemd) or sudo systemctl svrjs restart
(if you don't use systemd) command.
If you open a web browser, type in https://10.0.0.2/
(replace 10.0.0.2
with your server's IP address or hostname), and type in username and password to log into the web application, you will see the to-do list web application you just developed. You can add tasks, mark them as complete, and delete them.
You have now deployed the web application with SVR.JS web server to the production server.
Conclusion
In this blog post, we explored the process of building a JavaScript-based web application using the MERN stack (MongoDB, Express, React, Node.JS) and integrating it with the SVR.JS web server. We started by understanding the fundamentals of Node.js and SVR.JS, highlighting their key features and advantages.
We then dived into the practical steps of setting up the backend with Express and MongoDB, ensuring that our application could handle CRUD operations efficiently. We also set up the frontend using React and Vite, incorporating Bootstrap for a responsive and visually appealing design.
Throughout the development process, we emphasized best practices such as using environment variables, setting up ESLint and Prettier for code quality, and configuring a development server with nodemon for a seamless development experience. We also ensured that our application was secure by implementing HTTP authentication with SVR.JS.
By serving the frontend with SVR.JS's static file serving capabilities and configuring HTTP authentication, we added an extra layer of security and efficiency to our application. This integration not only enhanced the performance but also ensured that our application was protected from unauthorized access.
Finally, we walked through the deployment process, ensuring that our application was ready for a production environment. We covered setting up a server, installing necessary dependencies, configuring MongoDB, and securing our application with HTTPS and HTTP authentication.
In summary, this blog post provided a comprehensive guide to building, integrating, and deploying a MERN stack to-do list application with SVR.JS. By following these steps, you can create robust, scalable, and secure web applications that leverage the power of modern JavaScript technologies and the flexibility of SVR.JS.
Whether you are a beginner or an experienced developer, the knowledge and techniques shared in this post will help you build efficient and reliable web applications. Happy coding!