Tutorial: Modern Multi-Service Full-Stack Web App
Using Nginx, a frontend website, and backends built with Node.js and Rust.
Contents:
This tutorial helps you set up a web server that hosts the following:
- A frontend website (HTML, CSS, PHP, JS)
- A Node.js backend (service)
- A Rust backend (service)
- Nginx as reverse proxy and frontend server
What's new?
The browser communicates only with Nginx, which decides what happens:
- Frontend requests → HTML/CSS/PHP/JS files
- API requests → forwarded to the Node.js or Rust backend
Step 1: Frontend Website
In this example, the website files are located at:
/var/www/testlab.computerbas.nl/public/
Files:
index.html
styles.css
app.js
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<title>My first Multi-Service Full-Stack WebApp</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Multi-Service Full-Stack WebApp</h1>
<button id="nodeBtn">Get Data from Node.js</button>
<button id="rustBtn">Get Data from Rust</button>
<pre id="results"></pre>
<script src="app.js"></script>
</body>
</html>
app.js:
document.addEventListener('DOMContentLoaded', () => {
const nodeBtn = document.getElementById('nodeBtn');
const rustBtn = document.getElementById('rustBtn');
const resultsDiv = document.getElementById('results');
nodeBtn.addEventListener('click', async () => {
const response = await fetch('/api/nodejs/data');
const data = await response.json();
resultsDiv.textContent = JSON.stringify(data, null, 2);
});
rustBtn.addEventListener('click', async () => {
const response = await fetch('/api/rust/data');
const data = await response.json();
resultsDiv.textContent = JSON.stringify(data, null, 2);
});
});
Step 2: Backend services
These backend apps run locally (127.0.0.1) on different ports and are not directly accessible to the public.
A) Node.js API
- Create a directory, for example
/home/bas/nodejs/ - Run:
npm init -yandnpm install express - Create a file called
server.js
server.js:
const express = require('express');
const app = express();
const PORT = 3002;
app.get('/data', (req, res) => {
res.json({ service: 'Node.js', message: 'Hello from the Node API!' });
});
app.listen(PORT, '127.0.0.1', () => {
console.log(`Node.js API server listening on http://127.0.0.1:${PORT}`);
});
B) Rust API
- Create a new project:
cargo new rust-app - Add Axum and Tokio:
cargo add axum tokio -F tokio/full
Example: src/main.rs
use axum::{routing::get, Json, Router};
use serde_json::{json, Value};
async fn get_data_handler() -> Json<Value> {
Json(json!({
"service": "Rust",
"message": "Hello from the Rust API!"
}))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/data", get(get_data_handler));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3003")
.await
.unwrap();
println!("Rust API listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
Step 3: Managing Services with systemd
Create systemd service files so the backends start and restart automatically.
nodejs.service
[Unit]
Description=Node.js API Service
After=network.target
[Service]
User=bas
WorkingDirectory=/home/bas/nodejs/
ExecStart=/home/bas/nodejs/server.js
Restart=always
[Install]
WantedBy=multi-user.target
rust.service
[Unit]
Description=Rust API Service
After=network.target
[Service]
User=bas
WorkingDirectory=/home/bas/rust
ExecStart=/home/bas/rust/rust-app
Restart=always
[Install]
WantedBy=multi-user.target
Step 4: Configuring Nginx as a Reverse Proxy
Create a configuration file:
sudo nano /etc/nginx/sites-available/testlab.conf
Example Configuration:
server {
listen 80;
server_name testlab.computerbas.nl;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name testlab.computerbas.nl;
root /var/www/testlab.computerbas.nl/public;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
location /api/nodejs/ {
proxy_pass http://127.0.0.1:3002/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/rust/ {
proxy_pass http://127.0.0.1:3003/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Result
A fully functional multi-service web application.
- Frontend served by Nginx
- Node.js and Rust backends communicate via API routes
- Secure, scalable, and easier to maintain