From Scratch to Server: Creating and Hosting a Node.JS App with SVR.JS

From Scratch to Server: Creating and Hosting a Node.JS App with SVR.JS

Published on: December 30, 2024

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:

Blog Image

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:

Blog Image

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:

Blog Image

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:

Blog Image

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!