This tutorial shows how to create a Node.js server with Express.js in JavaScript, implementing Basic Authentication to secure a /user
endpoint. The server authenticates users against a userlist.json
file, supports CORS for a React Client at localhost:5173
, and mimics the functionality of a TypeScript HTTP server. Perfect for learning Express and authentication!
Prerequisites
- Node.js (LTS, e.g., 20.x): Install from nodejs.org and verify:
node -v npm -v
- Text editor: VS Code or IntelliJ.
- Terminal: For commands.
- Angular client (optional): Running at
http://localhost:5173
(see Building a Simple React App with Basic Authentication).
Step 1: Set Up the Project
Create a project folder and initialize it:
mkdir nodejs-express-auth
cd nodejs-express-auth
npm init -y
Install Express.js:
npm install express
Create server.js:
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
Optional: Configuring nodemon and browser-sync for Live Reloading
Now that you’ve built your Express “Hello World” server, you might have noticed that changes to server.js
require a manual restart and browser refresh. To streamline development, we can configure nodemon
for server-side hot reloading and browser-sync
for automatic browser refreshes. This chapter guides you through the setup, perfect for enhancing your productivity as a developer working on Node.js applications.
Why Use nodemon and browser-sync?
- nodemon: Monitors your Node.js files and restarts the server automatically when changes are detected, saving you from manual restarts.
- browser-sync: Proxies your Express server and reloads the browser when files change, providing a seamless development experience.
- Together, they’re ideal for rapid iteration, aligning with modern development practices you might use in Angular or AWS deployments.
Install Required Packages
Install nodemon
, browser-sync
, and concurrently
(to run both tools simultaneously):
npm install --save-dev nodemon browser-sync concurrently
- Add
--save-dev
to include them indevDependencies
inpackage.json
.
Update package.json
Modify your package.json
to include a development script:
{
"scripts": {
"start": "node server.js",
"dev": "concurrently \"npx nodemon server.js\" \"browser-sync start --proxy http://localhost:3000 --files *.js\""
}
}
"start"
: Runs the server normally."dev"
: Usesconcurrently
to runnodemon
(vianpx
to avoid global installation) andbrowser-sync
together.--files server.js
: Tellsbrowser-sync
to watch onlyserver.js
for changes (adjust if monitoring multiple files).
Run the Development Server
Start the setup with:
npm run dev
- Expect output like:
[0] [nodemon] starting
server.js“[1] [Browsersync] Proxying: http://localhost:3000
[1] Local: http://localhost:3001
(browser-sync UI)
- Open
http://localhost:3001
in your browser to see the proxied server.
Test Live Reloading
- Edit
server.js
(e.g., changeres.send('Hello World!')
tores.send('Hello Updated World!')
):
const express = require('express'); const app = express(); const port = 3000; app.get('/', (req, res) => { res.send('Hello Updated World!'); }); app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); });
- Save the file.
nodemon
will restart the server, andbrowser-sync
will refresh the browser automatically.
Troubleshooting Common Issues
- “nodemon: command not found”:
- Ensure
nodemon
is installed. If usingnpx
fails, install globally:npm install -g nodemon
- Then update the script to
"dev": "concurrently \"nodemon server.js\" \"browser-sync start --proxy http://localhost:3000 --files server.js\""
.
- Ensure
- Browser Not Reloading:
- Verify
browser-sync
is proxying the correct port (e.g., matchport
inserver.js
). - Check the
--files
flag; use--files *.js
to watch all.js
files if needed.
- Verify
- Port Conflict:
- If port 3000 is in use, change it in
server.js
(e.g.,const port = 8080
) and update the--proxy
flag.
- If port 3000 is in use, change it in
- Deprecation Warnings:
- Ignore
[DEP0060]
frombrowser-sync
for now; update to the latest version (npm install browser-sync@latest
) if persistent.
- Ignore
Optional Enhancements
- Custom nodemon Configuration:
- Create
nodemon.json
to watch specific files:{ "watch": ["server.js"], "ext": "js" }
- Create
- Multiple File Watching:
- Adjust
--files
tosrc/*.js
if you moveserver.js
to asrc
folder later.
- Adjust
- Docker Integration:
- For AWS deployment, add
nodemon
to yourDockerfile
:RUN npm install -g nodemon CMD ["npm", "run", "dev"]
- For AWS deployment, add
Step 2: Create the User List
Create userlist.json
in the project root:
[
{
"username": "admin",
"password": "password123",
"role": "admin"
},
{
"username": "user",
"password": "secret",
"role": "user"
}
]
Step 3: Write the Server Code
Update server.js
:
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
console.log('Express application initialized');
const userListPath = path.join(__dirname, 'userlist.json');
console.log('User list path set to:', userListPath);
// Middleware for CORS (handles preflight OPTIONS)
app.use((req, res, next) => {
console.log('CORS middleware triggered for request:', req.url);
if (req.method === 'OPTIONS') {
console.log('Handling OPTIONS preflight request for:', req.url);
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
return res.status(204).end();
}
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
next();
});
// Authentication middleware (unchanged)
const authenticate = (req, res, next) => {
console.log('Authentication middleware started for request:', req.url);
const authHeader = req.headers.authorization;
if (!authHeader) {
console.log('No authorization header found');
res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');
return res.status(401).end();
}
console.log('Authorization header received:', authHeader);
const base64Credentials = authHeader.split(' ')[1];
const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
console.log('Decoded credentials:', credentials);
const [username, password] = credentials.split(':');
let users = [];
try {
console.log('Attempting to read userlist.json');
const data = fs.readFileSync(userListPath, 'utf8');
users = JSON.parse(data);
console.log('User list loaded successfully:', users);
} catch (err) {
console.error('Error reading userlist.json:', err);
return res.status(500).send('Server error');
}
console.log('Searching for user:', username);
const user = users.find(u => u.username === username && u.password === password);
if (!user) {
console.log('No matching user found for:', username);
res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');
return res.status(401).end();
}
console.log('User authenticated:', user);
req.user = { username: user.username, role: user.role };
next();
};
// Routes
app.get('/user', authenticate, (req, res) => {
console.log('Processing /user route for user:', req.user.username);
res.json({ username: req.user.username, role: req.user.role });
});
app.get('/', (req, res) => {
console.log('Processing / route');
res.send('Public page: Hello World');
});
const PORT = 3000;
app.listen(PORT, () => {
console.log('Server listening on port:', PORT);
console.log(`Server running at http://localhost:${PORT}/`);
});
What’s Happening?
- Express: Simplifies routing and middleware compared to raw HTTP.
- CORS: Allows Angular at
localhost:4200
to connect. - Authentication: Checks
Authorization
header (Basic <Base64(username:password)>
) againstuserlist.json
. - Routes:
/user
: Protected, returns{ username, role }
./
: Public, returns “Hello World”.
- Preflight: Handles
OPTIONS
requests for CORS.
Hide secrets such as API keys in your server (optional)
Dealing with secrets in web applications such as API keys is often challenging as web browsers are not very good at keeping secrets. Exposing an API key directly in client-side code (e.g., a React or Angular app) risks it being accessed via browser developer tools or intercepted by malicious scripts. For this reason, the only place to keep your information secure is on the server. This section provides a comprehensive guide to enhancing your Node.js Express authentication server by adding a endpoint for the xAI API wrapper, ensuring the API key remains secure. By wrapping the xAI API in a server-side Node.js application, you:
- Protect the API Key: Store the key securely in server-side environment variables, inaccessible to clients.
- Control Access: Implement authentication (e.g., Basic Auth, as in your server) to restrict API usage.
- Enhance Flexibility: Allow for additional logic, such as rate limiting or request validation, before forwarding to the xAI API.
- Build Trust: Clearly communicate the security benefits to clients, fostering confidence in your solution.
Implementation Steps
- Store the xAI API Key Securely:
- Use the dotenv package to manage environment variables.
- Install: bashCollapseWrapRunCopy
npm install dotenv
- Create a .env file in your project root: envCollapseWrapCopy
XAI_API_KEY=your-xai-api-key
- Ensure .env is in .gitignore to prevent exposure.
- Update
server.js
with a Endpoint:
...
app.post('/send-message', authenticate, async (req, res) => {
try {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', async () => {
const { message, max_tokens = 100, temperature = 0.7 } = JSON.parse(body);
console.log(`Received /send-message: message="${message}", max_tokens=${max_tokens}, temperature=${temperature}`);
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
const grokApiUrl = 'https://api.x.ai/v1/chat/completions';
const grokApiKey = process.env.GROK_API_KEY;
try {
const grokResponse = await axios.post(
grokApiUrl,
{
model: "grok-2-1212",
messages: [{ role: "user", content: message }],
temperature,
max_tokens,
stream: false,
},
{
headers: {
'Authorization': `Bearer ${grokApiKey}`,
'Content-Type': 'application/json'
}
}
);
res.json({ response: grokResponse.data });
} catch (apiErr) {
console.error('Error calling Grok API:', apiErr);
console.error('Error calling Grok API:', apiErr.response?.data || apiErr.message);
res.status(502).json({ error: 'Failed to get response from Grok API' });
}
});
} catch (err) {
console.error('Error in /send-message:', err);
res.status(500).json({ error: 'Server error' });
}
});
...
Step 4: Run the Server
Start the server:
node server.js
See: Server running at http://localhost:3000/
.
Step 5: Test the Server
- Browser:
- Visit
http://localhost:3000/
→ “Public page: Hello World”. - Visit
http://localhost:3000/user
→ Enteradmin:password123
→{"username":"admin","role":"admin"}
. Wrong credentials show “Invalid credentials”.
- Visit
- cURL:
curl http://localhost:3000
# Public: "Public page: Hello World"
curl -u admin:password123 http://localhost:3000/user
# Secure: {"username":"admin","role":"admin"}
curl http://localhost:3000/user
# No auth: "Authentication required"
Security Notes
- JSON File: Replace with a database (e.g., PostgreSQL) in production.
- HTTPS: Use HTTPS to secure Basic Auth credentials.
- Error Handling: Add robust validation for
userlist.json
. - Rate Limiting: Use
express-rate-limit
to prevent brute-force attacks.