Tutorial: CRUD Rest API with NodeJS, MariaDB and NGINX

. Tutorial: Create a Rest API, CRUD with NodeJS, MariaDB, and Nginx on Unraid

This tutorial is about making a Rest API web server using NodeJS with create, read, update, delete (CRUD) capability connected to a backend MariaDB database, reverse proxy and Cors managed by NGINX Proxy Manager. In this tutorial the Rest API web server is written in NodeJS using Visual Studio Code on a Windows computer and MariaDB Server and Nginx Proxy Manager are running in Docker containers on Unraid.

Tutorial nginx proxy manager nodejs maria db

I've noticed quite a few tutorials want to remove the complexity of connecting to an actual database. Unfortunately while making writing the tutorial easier to write and implement I do not believe having a text file or data written to a local array is a particulary realistic scenario.

So while increasing the complexity you get a much better learning experience by connecting to a real database.

Please be aware that this example uses a specific set of software and most importantly NGINX to request the SSL certificate and run a reverse proxy. If you don't use NGINX then just modify your set up to use whatever alternative reverse proxy software you are using, eg, Caddy.


Pre-requisites

For this example I am running:

  • A computer running Windows 11.
  • Visual Studio Code v1.101.2 (installed on Windows PC).
  • MariaDB Server (v11.4.5) running in a Docker container on Unraid.
  • NGINX Proxy Manager running in a Docker container on Unraid.
  • Postman.
  • HeidiSQL.
  • Unraid 6.2.10

Background: REST API's

In the next three paragraphs I'll briefly explain what REST API's are. Feel free to skip if you just want to dive in and get into the nitty gritty.

A REST API (Representational State Transfer Application Programming Interface) is a standardized interface used to enable communication between computer systems over the internet. It is based on a set of principles and constraints that define how resources - such as data objects or services - can be accessed and manipulated using standard HTTP methods like GET, POST, PUT, PATCH, and DELETE. REST APIs treat everything as a resource, which can be uniquely identified by a URL, making them intuitive and easy to understand.

CRUD: You may also have heard of CRUD. This acronym stands for "Create, Read, Update and Delete". It's a piece of jargon. How this lines up with REST API's is shown in the table below:

CRUD HTTP
CREATE PUT
READ GET
UPDATE PUT or PATCH
DELETE DELETE

In a RESTful architecture, clients (like web browsers or mobile apps) send HTTP requests to the API server, which processes these requests and returns responses typically in formats like JSON or XML. For example, a client might make a GET request to /customers to retrieve a list of customers or send a POST request to /orders with order details to create a new order. This separation of client and server responsibilities enables REST APIs to be stateless, meaning each request must contain all the necessary information for the server to process it, without relying on stored context from previous interactions.

REST APIs are widely used due to their simplicity, scalability, and compatibility with the web. They allow different applications and systems—regardless of programming language or platform - to communicate and share data efficiently. This makes REST an ideal choice for building modern web services, powering everything from e-commerce platforms and social media to IoT devices and enterprise software integrations.

This truly is quite a huge project with multiple moving parts even for the simplest of web servers. It's got everything from port forwarding on your router, reverse proxy via NGINX Proxy Manager, Cross-Origin Resource Sharing (Cors), dynamic DNS (via DuckDNS), SSL encryption (using LetsEncrypt and NGINX Proxy Manager), Unraid, NodeJS, HTML, javascript and lots and lots of testing.

High level process

This is quite the gargantuan project and is best broken down into its smaller components. At the very end you will see how it all comes together. However, the main steps can be roughly defined as:

Conceptually a top down view of the data flow in this tutorial looks like this:

data flow chart

Looking at the above diagram we will be:
- Creating the REST API web server with NodeJS (the yellow circle) on a local development PC
- Requesting data from the web server from a webpage at the domain www.50plusjourney.com
- Using Nginx for reverse proxy, applying cors and requesting a SSL certificate
- The REST API webserver will action commands on the MariaDB data store
- Using DuckDNS to cater for a non-static home IP address

Development Computer Running Windows

A critical component of this tutorial and in fact any tutorial involving NGINX Proxy Manager and making data available outside your local network and out into the world wide web are IP addresses and ports. Yes, everything can get quite messy when you are problem solving as any networking person will know.

Development vs Production: Anyway, why this is important as in this particular project the web server is only a "development" server and not rolled out into "Production" into it's own Docker Container on Unraid. The key here is that the "development" computer is effectively running on a separate network to NGINX Proxy Manager that is running on Unraid.

IP Address of development computer: This means that one of the working parts and essential components of this project is knowing the IP Address of the development computer. Hopefully you have a "static" IP set up otherwise you will have to continually keep track of the IP address when you do your testing further down the track.

As the Development computer is running Windows you can find the IP address of the computer in the Windows Terminal by typing:

Code
📋
ipconfig

windows ip address

Your IP Address: The key here is to look for the IPv4 address as your development computer is known on your network either by an ethernet adaptor or as per this example a wireless adaptor. Whatever the case, note down the IP address as this will be used later. As stated earlier, hopefully your network uses static IP addresses so you won't have to keep running Ipconfig to geet your IP Address.

Node.js Web Server

In the following I will create the most simple Node.js web server ever (or close to it). The basic NodeJS web server will then be expanded to create a Rest API webserver.

Pre-requisite: "Visual Studio Code" must be installed on the development computer as the Node.js webserver will be coded in Visual Studio Code.



visual studio code logo

Ports and IP Addresses: Yeah, don't want it to be a beatdown but this is critical data required for creating a successful project.

In this tutorial the webserver will be running on port: 8100.

IP Address: As outlined earlier the Node.js web server will be running on the development computer rather than it's own Docker Container. As such, you have to know the IP address of the development computer. In this tutorial, the IP Address of the development computer is: 192.168.1.111.

NodeJS REST API Web Server: Step by Step

All the following will be performed in Visual Studio Code. The high level process is listed below:

  1. Create a new folder for your NodeJS web server project.
  2. Initialize a Node.js project. In the Terminal run code: npm init -y
  3. Install Middleware dependencies. In the Terminal run code: npm install express mariadb
  4. In the project folder create a new file called 'server.js'
  5. In the file 'server.js' code the simple NodeJS web server.
  6. Run the NodeJS web server from the Terminal with the code: node server.js
  7. Test the NodeJS server is running.

Step 1: Create project folder

create project folder

Step 2: Initialize a Node.js project

In the Terminal run code: npm init -y

Initialize a Node.js project

Step 3: Install Middleware dependencies

In the Terminal run code: npm install express mariadb.

This installs the 'Express' middleware, a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.

You also install the MariaDB middleware that facilitates communication between the Node.js application and the MariaDB database you will connect to.

Install Middleware dependencies

Step 4: In the project folder create a new file called 'server.js'

server.js

Step 5: Code simple NodeJS web server

Write the code for the NodeJS web server in Visual Studio Code noting that in this example the server will be running on port: 8100 and on the development computer with IP address: 192.168.1.111.

Code
📋

const express = require('express');
const app = express();
const PORT = 8100;

app.get('/', (req, res) => {
  res.json({ message: 'Hello from API on port 8100!' });
});

app.listen(8100, '192.168.1.111', () => {
  console.log('API running on http://192.168.1.111:8100');
});

In Visual Studio Code, it will look like this:

Visual Studio Code

The program can be broken down as follows:

const express = require('express');
- Loads the Express.js library.
- Express simplifies building HTTP servers in Node.js — handling routes, middleware, JSON responses, etc.

const app = express();
- Creates an instance of an Express application.
- This app object is what you'll use to define routes and middleware.

const PORT = 8100;
- Just a constant for the port number the server will listen on.
- Used later in app.listen().

app.get('/', (req, res) => { ... });
- Defines a GET route for the path / (the homepage).
- When someone visits http://192.168.1.111:8100/, this function is called.
- Inside the callback it sends a JSON response like
{ "message": "Hello from API on port 8100!" }

app.listen(8100, '192.168.1.111', () => { ... });
- Starts the server and listens for connections on:
- Port 8100
- IP address 192.168.1.111
This binds the server only to that specific IP (not to all interfaces).

The callback logs this message to confirm:
API running on http://192.168.1.111:8100

Step 6: Start the NodeJS web server

In the Terminal type: node server.js to start the web server.

Start the NodeJS web server

In the terminal window you will get a message that the API is running.

Step 7: Test the NodeJS web server is running

In your web browser on the development computer type: : node server.js to start the web server.

Test the NodeJS web server is running

Congrats! You've successfully coded a super simple NodeJS web server.

Add RESTful API functionality to Web Server

The basic web server is just that, basic. However, this tutorial is both a tutorial on RESTful API's and networking. Depending on who you are it may be worth getting all the networking side of this tutorial working before adding the REST API functionality to the web server. This will help you resolve any issues with debugging your REST API later on by compartmentalizing network issues from API issues.

In fact, I'd definitely suggest making sure the basic web server is accessible external to your network before you begin coding the API but I'll leave that up to you to decide.

MariaDB: In this tutorial the REST API will be accessing data in a MariaDB database. As such you will have to have MariaDB Server running and the test database setup. I have conveniently written SQL scripts for you to create the testdb database, database tables and dummy data in my tutorial on MariaDB here.

The first step is to create your initial constants (see code below):

Code
📋
const express = require('express');
const mariadb = require('mariadb');
const app = express();
const port = 8100;

Connect to your MariaDB database: The next step is setting up the connection to your MariaDB database. The following code segment is required to connect to your MariaDB database. As per the image you have to provide:

  • Host IP adress
  • Port
  • User
  • Password
  • Database
  • ConnectionLimit

Code
📋
// 2. Create a connection pool
const pool = mariadb.createPool({
host: '192.155.1.150', // IP of Unraid Server
port: 3306, // Port of MariaDB Docker on Unraid Server
user: 'root',
password: 'Barbie2025',
database: 'testdb',
connectionLimit: 25
});

Host IP: In our tutorial as the MariaDB is running in a Docker container on our Unraid Server, the Host is the IP address of the Unraid Server.

Port: The port is the port assigned in the MariaDB Docker container.

The remainder of the variables should be self-explanatory noting in this tutorial we will connect to the MariaDB 'testdb' database and it should be populated using the SQL scripts provided in my MariaDB tutorial here.

Helper Code: The next step is adding what I call helper code. The helper code segments are what can be regarded as an aside (but a very useful one). I include them as they get around issues that may trip you up so you can focus on what you really want to learn about - REST API's.

Code
📋
/////////////////////////////////////////////////////
//// Add this middleware to your express server ////
//// If your req.body is undefined or null /////
/////////////////////////////////////////////////////

// For parsing application/json - for parsing JSON request bodies.
app.use(express.json());

// For parsing application/x-www-form-urlencoded
// For parsing URL-encoded request bodies (like data from HTML forms)
app.use(express.urlencoded({ extended: true }));

//////////////////////////////////////////////////////////
//// EO: Add this middleware to your express server ////
//// If your req.body is undefined or null /////
//////////////////////////////////////////////////////////

Code
📋
////////////////////
// BIGINT Fix //
////////////////////

// Replacer: if the value is a BigInt, convert to string; otherwise leave it alone.
function bigintReplacer(key, value) {
return typeof value === 'bigint'
? value.toString()
: value;
};

// tell Express to use this replacer for all res.json() calls
app.set('json replacer', bigintReplacer);

////////////////////
// EO: BIGINT Fix //
////////////////////

Okay with the "Helper" code out of the way we can begin all the Rest API material that you are really interested in. First, I'll provide you some information on NodeJS and Endpoints that will help you understand the code I will take you through.

You can skip this background information if you wish but knowledge is power as they a say.

Endpoints

In the context of a Node.js–powered REST API, an endpoint is simply a specific URL (or “route”) combined with an HTTP method that your server exposes to clients. Each endpoint corresponds to one discrete operation on a resource.

Key characteristics of an endpoint

  • HTTP Method
  • Specifies the kind of action you’re performing:

    • GET – Retrieve a resource or list of resources
    • POST – Create a new resource
    • PUT / PATCH – Update an existing resource
    • DELETE – Remove a resource
  • Path (Route)
  • The URL pattern that identifies which resource(s) you’re talking about, for example:

    • /users
    • /customers/:customerid
    • /products/:productid
    • /albums/:albumid
    • /orders/:orderid/items
  • Handler Function
  • The piece of code that runs when a request matches that method+path, taking care of validation, business logic, and sending back an HTTP response.

Why Endpoints Matter

  • Organization: Breaking your API into clear, method-specific routes makes it easier to reason about what each URL does.
  • Documentation: Tools like Swagger/OpenAPI can automatically generate docs by inspecting your endpoints.
  • Maintainability: If you need to change how “create user” works, you know exactly to look at your POST /users handler.

Endpoints: In summary.

An endpoint in a Node.js REST API is the combination of:

  • An HTTP method
  • A URL path
  • A piece of code (handler) that runs when that route is requested

Together, these define exactly how clients interact with your server’s resources. With all the theory out of the way let the coding begin.

GET endpoint

I will provide two GET examples. The first will example doesn't take any parameters and the second will take one parameter.

Example 1a: GET endpoint (no parameters)

In this example we extract all the records from the 'customers' table (no parameters required).

Code
📋
app.get('/customers', async (req, res, next) => {
const conn = await pool.getConnection();
try {
const rows = await conn.query('SELECT * FROM customers');
res.json(rows);
} finally {
conn.release();
}
});

Error handling: The above code uses next so any thrown errors bubble up to your (global) error handler.

All following examples will include self-contained errorhandling rather than relying on your global error handler.

Example 1b: GET endpoint (no parameters and local error handling)

We will extract all the records from the 'customers' table (no parameters required) with local error handling.

Code
📋
// GET endpoint to fetch all customers
app.get('/customers', async (req, res) => {
let conn;
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT * FROM customers');
res.json(rows);
} catch (err) {
console.error('Database error:', err);
res.status(500).json({ error: 'Failed to fetch customers' });
} finally {
if (conn) conn.release();
}
});

Testing Endpoints: You can test the GET endpoint using Postman.

Postman logo api tester

Postman is a widely used API (Application Programming Interface) testing tool that simplifies the process of creating, testing, and documenting APIs. It provides a user-friendly interface for interacting with APIs, allowing developers and testers to send requests, receive responses, and validate API behavior without writing extensive code.

Input Postman as per the image below. Note the URL input into Postman and how it reflects your coded GET endpoint. Note: In this case the IP address is the IP address of your local development PC.

get endpoint

>Example 2: GET endpoint (with a single parameter)

We will extract a record from the 'customers' table using a single parameter of 'customerid'. Note: Contains self-contained error handling.

Code
📋
// GET endpoint with 1 parameter(s)
app.get('/customers/:customerid', async (req, res) => {
let conn;
try {
conn = await pool.getConnection();

const customerid = req.params.customerid;
// Use a prepared statement to prevent SQL injection
const rows = await conn.query('Select * FROM customers WHERE customerid = ? ', [customerid]);
res.json(rows);
} catch (err) {
console.error('DB error:', err);
res.status(500).json({ error: 'Failed to fetch events' });
} finally {
if (conn) conn.release();
}
});

Input Postman as per the image below. Note the URL input into Postman also includes your parameter value for customerid and how it reflects your coded GET endpoint. Note: In this case the IP address is the IP address of your local development PC.

Postman get endpoint

As per this example we have chosen to select the record where customerid = 1 and you can see the returned value in JSON format. You can verify the data by running the same query yourself in HeidiSQL. See the results below.

If you are unfamiliar with HeidiSQL read my Tutorial on Creating a MariaDB here.

REST API NodeJS Postman GET http

POST endpoint

I will provide one POST endpoint example. We will provide the data for a new product and add it in the product table. In our example, a product has three pieces of data that define a product:

  • productname
  • category
  • brand

Example 1: POST endpoint. We will add a product to the products table. Note: Contains self-contained error handling.

Assuming you are using the 'testdb' and dummy data we have provided the SQL scripts for the initial data in the 'products' table is:

products table

I am showing this data so you can see the record that we will add. As you can see, initially there are nine records in the table.

The code for the POST endpoint is shown below:

Code
📋
// POST endpoint with 3 body input(s)
app.post('/productadd', async (req, res) => {

const productname = req.body.productname;
const category = req.body.category;
const brand = req.body.brand;

// Basic validation
if (!productname || !category || !brand) {
return res.status(400).json({ error: 'productname, category, brand are all required' });
}

try {
// Parameterized INSERT - Use a prepared stmt to prevent SQL injection
const result = await pool.query(
'INSERT INTO products (productname, category, brand) VALUES ( ?, ?, ? )',
[productname, category, brand]
);

// result.insertId contains the new row's auto-generated ID
res.status(201).json({
message: 'Record created',
Id: result.insertId
});

} catch (err) {
console.error('DB error inserting product:', err);
res.status(500).json({ error: 'Failed to create product' });
}
});

Testing a POST Endpoint: Using Postman. To test a POST is more difficult than a GET as you must send a request body. So you can test the API Endpoint one of the most common testing tools is Postman. If you haven't already installed it follow this link.

This isn't a tutorial on how to use Postman but following our example you enter details as per this screen below. Importantly you will see that you have to enter a "request body" that contains the data for the product we are adding to the database.

Test post endpoint in postman

The important things to note are:

  • It must be set to a POST request.
  • The request has a "body", ie, the data you will sent to the API
  • The body data sent to the API has a format, in this example, JSON
  • POSTMAN will respond to your request. In this example you can see "201 Created".

Return to HeidiSQL and query the database to verify that the new record has been added.

heidisql query database

Congratulations, you have learnt how to create and test a POST endpoint in a REST API.

PATCH endpoint

I will provide one PATCH endpoint example. A PATCH is synonymous with an "UPDATE" if you know SQL. If you don't know SQL, a PATCH simply updates existing record(s) with data that you want to amend.

In our example, we have just found out that the record we added is incorrect and needs to be corrected with the following data:

Field Before After
productid 10 10
productname Golden Barbie Diamond Barbee
category Toy Toy
brand LV Cartiere

The changes we want to implement are highlighted in the 'After' column for productid = 10.

The code for the PATCH endpoint is shown below:

Code
📋
// PATCH endpoint with 2 body input(s)
// - expects JSON body: { productname, brand}
// - Endpoint must be tested with data sent in body in JSON format
// - This Endpoint has: 1 parameters
// - :productid from Endpoint URL selects which row(s) to update
app.patch('/patchproduct/:productid', async (req, res) => {

// Parameter Constant(s)
const productid = req.params.productid;

// Json Body Constant(s)
const productname = req.body.productname;
const brand = req.body.brand;

// Basic validation
if (!productname || !brand || !productid) {
return res.status(400).json({ error: 'productname, brand, productid are all required' });
}

try {
// UPDATE - Use a prepared stmt to prevent SQL injection
const result = await pool.query(
'UPDATE products SET productname = ? , brand = ? WHERE productid = ? ',
[ productname, brand, productid]
);

if (result.affectedRows === 0) {
// no row matched
return res
.status(404)
.json({ error: `No record found` });
}

res.json({
message: 'Updated successfully',
affectedRows: result.affectedRows
});
} catch (err) {
console.error('DB error updating:', err);
res.status(500).json({ error: 'Failed to update' });
}
});

To test the PATCH endpoint in Postman your data entry should look like this:

Rest API Patch

The important things to note are:

  • It must be set to a PATCH request.
  • The endpoint has one parameter.
  • The request has a "body", ie, the data you will sent to the API
  • The body data sent to the API has a format, in this example, JSON
  • POSTMAN will respond to your request. In this example you can see "200 OK".

For visual verification you can return to to HeidiSQL, query the database and the record has been updated.

Rest API PATCH nodejs

Congratulations, you have learnt how to create and test a PATCH endpoint in a REST API.

DELETE endpoint

I will provide one DELETE endpoint example. A DELETE will delete the rows from the database that meet your selection criteria.

Security: It goes without saying that the DELETE endpoint is dangerous and you should ensure adequate security is on place if you include DELETE endpoints in your REST API.

Example: In this example you will delete the record where customerid = 4 who is the customer with the name "Taylor Swift". You can check the contents of the table "customers" in HeidiSQL before the DELETE endpoint is run. Assuming you are using the supplied SQL scripts the contents of the 'customers' table is:

REST API Delete NodeJS

The code for the DELETE endpoint is shown below:

Code
📋
// DELETE endpoint
// - This Endpoint has: 1 parameter
// - :customerid from Endpoint URL selects which row(s) to update
app.delete('/deletecustomer/:customerid', async (req, res) => {

// Parameter Constant(s)
const customerid = req.params.customerid;

// Basic validation
if (!customerid) {
return res.status(400).json({ error: 'customerid is required' });
}

try {
// DELETE - Use a prepared stmt to prevent SQL injection
const result = await pool.query(
'DELETE FROM customers WHERE customerid = ? ',
[ customerid ]
);

if (result.affectedRows === 0) {
// no row matched
return res
.status(404)
.json({ error: `No record found` });
}

res.json({
message: 'Deleted successfully',
affectedRows: result.affectedRows
});
} catch (err) {
console.error('DB error updating:', err);
res.status(500).json({ error: 'Failed to update' });
}
});

To test the DELETE endpoint in Postman your data entry should look like this:

DELETE endpoint nodejs REST API Postman

In HeidiSQL check whether the record has been successfully deleted.

hedisql check delete endpoint

Congratulations, you have learnt how to create and test a DELETE endpoint in a REST API.

This means we have coded the common endpoints used in a REST API.

Complete Source Code: CRUD REST API

Piecing it all together, the complete code for the NodeJS REST API server is included below. This code can be segmented this way:

  • Setting up the initial constants
  • Creating a connection pool to connect to your MariaDB database
  • Helper code to get over some common problems
  • Endpoint code (GET, POST, PATCH, DELETE)
  • Start the server

Code
📋
  	
const express = require('express');
const mariadb = require('mariadb');
    
const app = express();
const port = 8100;

// 2. Create a connection pool
const pool = mariadb.createPool({
  host: '192.155.1.150',               // IP where MariaDB is running
  port: 3306,						   // Port MariaDB using
  user: 'root',						   // User logging into MariaDB
  password: 'your password',           // empty string for NULL password
  database: 'testdb',				   // your databasename
  connectionLimit: 25
});

/////////////////////////////////////////////////////
//// Add this middleware to your express server  ////
////    If your req.body is undefined or null   /////
/////////////////////////////////////////////////////

// For parsing application/json - for parsing JSON request bodies.
app.use(express.json()); 

// For parsing application/x-www-form-urlencoded
// For parsing URL-encoded request bodies (like data from HTML forms)
app.use(express.urlencoded({ extended: true })); 

//////////////////////////////////////////////////////////
////  EO: Add this middleware to your express server  ////
////      If your req.body is undefined or null      /////
//////////////////////////////////////////////////////////

////////////////////
//   BIGINT Fix   //
////////////////////

// Replacer: if the value is a BigInt, convert to string; otherwise leave it alone.
function bigintReplacer(key, value) {
  return typeof value === 'bigint'
    ? value.toString()
    : value;
};

// tell Express to use this replacer for all res.json() calls
app.set('json replacer', bigintReplacer);

////////////////////
// EO: BIGINT Fix //
////////////////////

// GET endpoint to fetch all customers
app.get('/customers', async (req, res) => {
  let conn;
  try {
    conn = await pool.getConnection();
    const rows = await conn.query('SELECT * FROM customers');
    res.json(rows);
  } catch (err) {
    console.error('Database error:', err);
    res.status(500).json({ error: 'Failed to fetch customers' });
  } finally {
    if (conn) conn.release();
  }
});

// GET endpoint with 1 parameter(s)
app.get('/customers/:customerid', async (req, res) => {
let conn;
try {
    conn = await pool.getConnection();

const customerid = req.params.customerid;
    // Use a prepared statement to prevent SQL injection
const rows = await conn.query('Select * FROM customers WHERE customerid = ? ', [customerid]);
res.json(rows);
  } catch (err) {
console.error('DB error:', err);
    res.status(500).json({ error: 'Failed to fetch events' });
  } finally {
    if (conn) conn.release();
  }
});

// POST endpoint with 3 body input(s)
app.post('/productadd', async (req, res) => {

    const productname = req.body.productname;
    const category = req.body.category;
    const brand = req.body.brand;

// Basic validation
if (!productname || !category || !brand) {
  return res.status(400).json({ error: 'productname, category, brand are all required' });
}


 try {
// Parameterized INSERT - Use a prepared stmt to prevent SQL injection
    const result = await pool.query(
    'INSERT INTO products (productname, category, brand) VALUES  ( ?, ?, ? )', 
    [productname, category, brand]
  );

    // result.insertId contains the new row's auto-generated ID
    res.status(201).json({
      message: 'Record created',
      Id: result.insertId
    });

  } catch (err) {
    console.error('DB error inserting product:', err);
    res.status(500).json({ error: 'Failed to create product' });
  }
});


// PATCH endpoint with 2 body input(s)
// - expects JSON body: { productname, brand}
// - Endpoint must be tested with data sent in body in JSON format
// - This Endpoint has: 1 parameters
// - :productid from Endpoint URL selects which row(s) to update 
app.patch('/patchproduct/:productid', async (req, res) => {

    // Parameter Constant(s)
    const productid = req.params.productid;
    
    // Json Body Constant(s)
    const productname = req.body.productname;
    const brand = req.body.brand;

// Basic validation
if (!productname || !brand || !productid) {
  return res.status(400).json({ error: 'productname, brand, productid are all required' });
}

 try {
    // UPDATE - Use a prepared stmt to prevent SQL injection
    const result = await pool.query(
    'UPDATE products SET productname = ? , brand = ?  WHERE productid = ? ', 
    [ productname, brand, productid]
  );

    if (result.affectedRows === 0) {
      // no row matched
      return res
        .status(404)
        .json({ error: `No record found` });
    }

    res.json({
      message: 'Updated successfully',
      affectedRows: result.affectedRows
    });
  } catch (err) {
    console.error('DB error updating:', err);
    res.status(500).json({ error: 'Failed to update' });
  }
});

// DELETE endpoint
// - This Endpoint has: 1 parameter
// - :customerid from Endpoint URL selects which row(s) to update 
app.delete('/deletecustomer/:customerid', async (req, res) => {

// Parameter Constant(s)
const customerid = req.params.customerid;

// Basic validation
if (!customerid) {
  return res.status(400).json({ error: 'customerid is required' });
}

 try {
    // DELETE - Use a prepared stmt to prevent SQL injection
    const result = await pool.query(
    'DELETE FROM customers WHERE customerid = ? ', 
    [ customerid]
  );

    if (result.affectedRows === 0) {
      // no row matched
      return res
        .status(404)
        .json({ error: `No record found` });
    }

    res.json({
      message: 'Deleted successfully',
      affectedRows: result.affectedRows
    });
  } catch (err) {
    console.error('DB error updating:', err);
    res.status(500).json({ error: 'Failed to update' });
  }
});

// Start the server
app.listen(port, () => {
    console.log(`Server listening at http://localhost:${port}`);
});
	
  

If you are only interested in the CRUD REST API and NodeJS coding then you are done and dusted. Congrats!

While you are at it, please support my channel and efforts as I've covered a lot and with more information and detail than the usual blog post.

However, this is a truly comprehensive tutorial and you can continue to expand your project with reverse proxy, ssl encryption and cors support. Read on!

Buy Reika Haruto a Coffee now

Please support this channel: Have I saved you minutes, hours or even days of scouring the internet and youTube to find an actual working solution.

It takes me time and effort to both find a working solution and then write everything up. Please consider buying me a coffee so I can keep producing useful content, especially if I've made your life easier. Cheers!


Believe it or not a coffee goes a long way so if I've helped you out a coffee would be great. Cheers!

Nginx

We will be using Nginx in this tutorial for both:

  • Requesting an SSL Certificate from LetsEncrypt
  • Reverse proxy
  • Application of Cors

Pre-requisite: You have Nginx installed on Unraid. For installation instructions if you do not have Nginx installed follow this tutorial.

This tutorial covers the required port forwarding (on your router) and gets us to the point where we can add a "proxy host" in Nginx. Adding a "proxy host" is required later in this tutorial.

What is a proxy host: In very simple terms, a proxy host acts as an intermediary server that receives client requests and forwards them to one or more backend servers, essentially acting as a "middleman". This is commonly implemented as a reverse proxy, where Nginx sits in front of the backend servers, hiding their details from the client.

Dynamic DNS and Catering for Non-static IP address

If you are a home user often your premises does not have a static IP address and your internet service provider is in the background updating your IP address in the background without you ever knowing. This probably isn't an issue from a day to day operational point of view.

Background: A dynamic (non-constant) IP address becomes an issue when we want to make our data available over the internet using a domain name, eg, google.com or apple.com. The domain name is using what is known as the DNS (Domain Name System) that translates human readable domain names (like google.com) into numerical IP addresses that computers use to identify each other on the internet.

The problem: If your IP address changes, and you're using a domain name, anyone trying to reach your device using that domain name will fail to connect because the DNS record is no longer accurate.

Solution: Dynamic DNS (DDNS) saves the day! A DDNS service acts as a bridge, constantly monitoring your device's IP address and updating the DNS records accordingly. This ensures that your domain name always points to the correct, current IP address, even if it changes frequently.

Pre-requisite: In this tutorial you have to have:

  • a DuckDNS account
  • a DuckDNS token (for your account)
  • a subdomain (for your DuckDNS account)

DuckDNS Account: In this example the user has a token as per the image below:

duckdns account

In our example, the sub-domain (of domain "duckdns.org") is "snowwhiteforeva". This has an associated token provided to you from DuckDNS.

duckdns sub-domain

Request SSL Certificate

We utilize NGINX to request an SSL Certificate from LetsEncrypt for our DuckDNS sub-domain. There are a few benefits of this approach:

  • LetsEncrypt is FREE!
  • We are already using NGINX for our reverse proxy
  • NGINX handles auto renewing of certificates for us
  • NGINX makes the process easy
  • We will be using NGINX for applying Cors

High Level Process - SSL Certificate Request

  1. Open NGINX.
  2. Click on SSL Certificates then Add SSL Certificate then Let's Encrypt.
  3. Add your Sub-domain Name and other required information then submit request.
  4. Your SSL certificate and files are returned by Let's Encrypt and available in NGINX.

Step by Step Process

Step 1: Open NGINX.

Step 2: Click on SSL Certificates --> Add SSL Certificate --> Let's Encrypt.

ssl certificate

lets encrypt ssl

Step 3: Add your Sub-domain Name

Enter your DuckDNS SubDomain Name with a prefix of *. and click on Add.

Enter your DuckDNS SubDomain Name with a prefix

DuckDNS token: Have the token for your DuckDNS subdomain ready as this is required to get your SSL certificate.

DuckDNS token

Then enter:

  • Email address.
  • Turn ON "Use a DNS Challenge".
  • Select DNS Provider "DuckDNS".
  • In "Credentials File Content" enter your DuckDNS token.
  • Click on "I Agree to the Let's Encrypt Terms of Service".
  • Click on "Save".

DNS Challenge

Step 4: A SSL certificate will be created by Let's Encrypt. As the SSL certificate isn't yet being used it has a status of "Inactive".

Mission accomplished! You have a wildcard SSL certificate for your DuckDNS sub-domain.

wildcard SSL certificate

Please support this channel: Have I saved you minutes, hours or even days of scouring the internet to find an actual working solution.

It takes me time and effort to both find a working solution and then write everything up. Please consider buying me a coffee so I can keep producing useful content, especially if I've made your life easier. Cheers!


Buy Reika Haruto a Coffee

Start using your SSL certificate: As the certificate was created using NGINX you can now use the certificate you created to add a Proxy Host.

Conversely, you can download the SSL certificate files if you are using NGINX as a tool to request the SSL certificates to be used elsewhere.


SSL certificate files

Downloaded SSL certificate files: At the time this post was written the downloaded zip files contains the following:

  • cert1.pem
  • chain1.pem
  • fullchain1.pem
  • privkey1.pem

NGINX: Add Proxy Host

It's taken quite the journey to get here but now you have the building blocks to create the "proxy host" for your web server.

The pieces you've put together are summarized:

  • Dynamic DNS (setup for a constantly changing IP address) and associated DuckDNS sub-domain
  • An wildcard SSL certificate (for your DuckDNS sub-domain)
  • NGINX installed

Step 1: In NGINX click on the "Add Proxy Host" command button.

Add Proxy Host

Fill in the form as per below noting:

  • The unique domain name that will be used to access our web server on the internet and using our sub-domain of duckdns 'snowwhiteforeva'
  • Scheme: https
  • IP address: 192.168.1.111 (in our example that is the IP of the development PC running the NodeJS web server)
  • Port: 8100 (the IP the web server is using)

Using this information it means that our web server will be available across the internet using the URL: test.snowwhiteforeva.duckdns.org.

web server

Step 2: Add an SSL certificate: Under 'Edit proxy Host' click on 'SSL' then do the following (see image below):

  • Select the wildcard SSL Certificate you created for *.snowwhiteforeva.duckdns.org
  • Click on 'Force SSL'
  • Click on 'Save'
Add an SSL certificate

Congratulations! You've created a "Proxy Host" in NGINX Proxy Manager.

Add Cross-Origin-Resource-Sharing (Cors)

If you want the data accessible to websites that are not on your own domain then you will have to add Cross-Origin-Resource-Sharing (Cors) support. If not, the chances are that the webpage that is used to access your web server will be rejected automatically by the web browser. This is a modern security feature that you cannot dodge.

As an aside you maybe thinking why does the NodeJS application not have any code to apply Cors. In this example we are about to pass all the Cors logic to NGINX proxy Manager. You could apply Cors in your NodeJS application instead but that is not this tutorial.

Now the next steps are arguably the most difficult in this tutorial. Cors is a fiddly and challenging security feature to comply with. NGINX Proxy Manager simplifies things for the novice or infrequent user for basic configuration. However, applying Cors through the gui is a fully manual process.

WARNING: Anyway, as a way of warning do not try and apply Cors headers by manually editing the proxy host configuration files. While you can save them temporarily, NGINX Proxy Manager will automatically over-write them.

Step 1: Edit Proxy Host Configuration

Click on 'Edit' so we can change add additional settings to the Proxy Host configuration file.

Edit Proxy Host Configuration

Step 2: Advanced Settings

In the pop-up form switch from 'Details' to the 'Advanced' tab near the top right of the form. We will be adding our Cors settings in the section labeled 'Custom Nginx Configuration'.

nginx Advanced Settings

Step 3: Add Cors custom settings

In the 'Custom Nginx Configuration' text box paste in the following code and click 'Save':

Code
📋
set $forward_scheme http;

location / {

# Handle preflight (OPTIONS) directly
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' 'http://www.50plusjourney.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Content-Type' 'text/plain charset=UTF-8' always;
add_header 'Content-Length' 0 always;
return 204;
}

# Apply CORS headers to all other requests
add_header 'Access-Control-Allow-Origin' 'http://www.50plusjourney.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;

# Proxy! to Node.js backend
include conf.d/include/proxy.conf;
}

These custom configuration settings can be broken down into three main components:
1. Protocol setting (that gets saved into the {server} block of the proxy host configuration file;
2. # Handle preflight (OPTIONS) directly (that gets saved into the {location} block)
3. # Apply CORS headers to all other requests (that gets saved into the {location} block)

Explanation of the Code: Protocol Setting

Code
📋
set $forward_scheme http;


set $forward_scheme http;: This line sets a variable named $forward_scheme to the value http. This variable is then used in the proxy_pass directive to determine the scheme (protocol) used for forwarding the request.

In Nginx Proxy Manager, the command set $forward_scheme http; within a proxy host file sets the scheme for forwarding requests to the backend server to http. This means that even if the client connects to the proxy via HTTPS, the proxy will forward the request to the backend server using HTTP. This is useful when the backend server only supports HTTP or when you want to handle SSL termination at the proxy level.

Example: If you have a website accessible via https://example.com, and you configure Nginx Proxy Manager to forward requests to a backend server on port 80 (HTTP) with this configuration, the request will be forwarded as http://backend_server:80.

Purpose: This configuration is common when you want to handle SSL termination at the proxy level. The client connects to the proxy via HTTPS, the proxy decrypts the traffic, and then forwards the unencrypted HTTP traffic to the backend server. This simplifies the backend server's configuration, as it doesn't need to handle SSL/TLS certificates and encryption itself.

This is the case in this tutorial where NGINX is doing the heavy lifting regards SSL, etc and the web server is as basic as it gets.

Alternative: If you want to forward requests with HTTPS to the backend server, you would change the set command to:
set $forward_scheme https;
and ensure the backend server is configured to accept HTTPS connections. That lesson is for another day.

Explanation of the Code: # Handle preflight (OPTIONS) directly

Code
📋
location / {

# Handle preflight (OPTIONS) directly
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' 'http://www.50plusjourney.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always
;
add_header 'Content-Type' 'text/plain charset=UTF-8' always;
add_header 'Content-Length' 0 always;
return 204;
}

This section of code handles CORS preflight (OPTIONS) requests.

if ($request_method = OPTIONS) { ... }
This checks if the incoming HTTP request is an OPTIONS request.
Browsers send this automatically as a preflight request before sending certain types of cross-origin requests.

add_header 'Access-Control-Allow-Origin' 'http://www.50plusjourney.com' always;
Tells the browser: "Yes, this server allows requests from http://www.50plusjourney.com."
Critical for CORS.

add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
Tells the browser which HTTP methods are allowed for the actual cross-origin request.

add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
Specifies what custom headers the browser is allowed to send in the real request.

add_header 'Access-Control-Allow-Credentials' 'true' always;
Allows cookies and HTTP auth headers (like Authorization) to be sent in cross-origin requests.

add_header 'Access-Control-Max-Age' 86400 always;
Tells the browser to cache the preflight result for 24 hours (86400 seconds), so it doesn't need to resend OPTIONS every time.

add_header 'Content-Type' 'text/plain charset=UTF-8' always;
Defines the content type of the response — plain text with UTF-8 encoding.

add_header 'Content-Length' 0 always;
Explicitly sets the response body length to 0.

return 204;
Responds with HTTP 204 No Content.
This tells the browser "OK, you're clear to send your real request."
No body, no errors, just silent approval.

This block is for Purpose
Preflight (OPTIONS) requests Tells the browser it's okay to make the real request
Avoids hitting your backend Saves resources and avoids unnecessary proxying
Ensures valid CORS headers are returned Prevents CORS errors in browser

Explanation of the Code: # Apply CORS headers to all other requests

Code
📋
# Apply CORS headers to all other requests
add_header 'Access-Control-Allow-Origin' 'https://www.50plusjourney.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;

}

This section of code applies CORS headers to normal (non-OPTIONS) requests, such as GET, POST, PUT, or DELETE.

add_header 'Access-Control-Allow-Origin' 'https://www.50plusjourney.com' always;
Tells the browser that this server allows requests from the origin http://www.50plusjourney.com.
Required for cross-origin requests to be permitted.>

add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
Declares which HTTP methods are allowed when the browser sends a cross-origin request.
Helps both preflight and normal requests know what’s allowed.

add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
Specifies which custom headers can be sent in the request.
This is necessary when your frontend sends headers like Authorization or Content-Type.

add_header 'Access-Control-Allow-Credentials' 'true' always;
Allows the browser to send cookies, session tokens, or Authorization headers in the request.
Must be set if you use credentials: 'include' in fetch() or AJAX.

Extra Learnings: Why the always keyword?
Ensures the header is added even when NGINX returns errors (e.g., 4xx or 5xx).
Without always, NGINX might omit CORS headers on error responses - which breaks browser behavior.

Extra Learnings: View Proxy Host Configuration Files

Viewing the Proxy Host Configuration files is useful to increase your understanding of what NGINX Proxy Manager is actually doing as this is abstracted away using the gui. This extra learning lets you see the format, contents and settings that are otherwise hidden from you.

Testing: It is not for changing settings "permanently". It can however be useful for "testing" settings as any changes are not kept permanently.

Step 1: Find Proxy Host Configuration Files

Remembering that in this tutorial we are running NGINX Proxy Manager on Unraid, navigate your Unraid server and find the location of the Proxy Host configuration files that you generate via the NGINX Proxy Manager gui. In this example, the Proxy Host configuration files are located in this folder (see image below):

Find Proxy Host Configuration Files

Each of the files in this folder represent a different Proxy Host that are added in the NGINX Proxy Manager front end.

Step 2: Find and Open the Configuration File for the Proxy Host created earlier in this Tutorial

The contents of your proxy host configuration file will look similar to this (see image below). Note the image is a subset and the file is a lot longer than that shown.

Open the Configuration File for the Proxy Host

Step 3: Modify Server Block

In the proxy host configuration file find the 'Server' block and add the line:
set $forward_scheme http;

Modify Server Block

Changes are NOT permanent: While you can even 'Save' changes don't think they are permanent. Any saved changes will get wiped away when you shut down and restart NGINX Proxy Manager. I found out the hard way!

Testing Cors

To test the application of Cors and whether it is working with your API it will require testing from a web browser. Even though you have previously tested the API endpoints in Postman this was not testing Cors.

You will have to set up some web pages and test from a web browser due to the following:

  • Postman won’t block the response even if CORS is mis‑configured - so you won’t see a “blocked by CORS” error.
  • Browsers enforce CORS. Once you know your server is sending the right headers (by checking in Postman), try it in a browser or via your frontend app to confirm it actually works.
  • Postman bypasses CORS, so it can’t “fail” a request for missing headers - but you can simulate preflight with OPTIONS + the right headers and verify your server’s Access-Control-Allow-* responses.

In this tutorial we have applied strict access to the web server with requests only accepted from the domain:
- https://www.50plusjourney.com

WARNING: To test that the web server is working you have to test the request from a computer running on a network separate to where the web server is running.

This means that if you are testing from a home environment either test from a mobile phone that is using it's own data (not a wireless connection to your home network) or from a computer wirelessly connected to your mobile phone via a hotspot (again using the phone's data plan and NOT connected to your home network via wifi.

Create a web page on your website that uses the domain you have designated with the response header set using 'Access-Control-Allow-Origin' and set to accept 'https://www.50plusjourney.com'

Testing Cors for each of your Endpoints

I am providing you with code to create individual webpages to test each of the endpoints created in this tutorial. You simply create a web page, paste the code and replace the base URL with your API's base URL. Search the code for "API_BASE_URL", replace with the Base url of your API, save and you are good to go.

  • Link to web page code for GET endpoint with one parameter
  • Link to web page code for POST endpoint
  • Link to web page code for PATCH endpoint
  • Link to web page code for DELETE endpoint

If you use the supplied html and javascript code your pages should look similar to these:

Test GET: Web page for testing Cors and the GET endpoint with one parameter.

Test Get endpoint cors

Test POST: Web page for testing Cors and the POST endpoint.

Test POST endpoint cors

Test PATCH: Web page for testing Cors and the PATCH endpoint.

Test PATCH endpoint cors

Test DELETE: Web page for testing Cors and the DELETE endpoint.

Test delete endpoint cors

Congratulations

Congratulations - You've completed one mammoth learning exercise. This tutorial has a bit of everything so if you have got it working, well done.

Buy Reika Haruto a Coffee now

Please support this channel: Have I saved you minutes, hours or even days of scouring the internet and youTube to find an actual working solution.

It takes me time and effort to both find a working solution and then write everything up. Please consider buying me a coffee so I can keep producing useful content, especially if I've made your life easier. Cheers!


Believe it or not a coffee goes a long way so if I've helped you out a coffee would be great. Cheers!

Post a Comment

0 Comments