04. Form Handling

This module covers how to receive and process form data in Node.js with Express. We'll compare the approach with PHP's superglobals and cover security considerations.

Demo Files

Run: npm install then node form-demo.js

Try it live: cse135.site:3003/demo - Test GET, POST, and JSON submissions

PHP vs Node.js: Form Data Access

In PHP, form data magically appears in superglobals. In Node.js/Express, you must explicitly configure middleware to parse it.

Scenario PHP Express
GET parameters
?name=Alice
$_GET['name'] req.query.name
POST form data $_POST['name'] req.body.name
(requires middleware)
JSON body json_decode(file_get_contents('php://input')) req.body
(requires middleware)

GET Parameters (Query Strings)

GET data is available immediately in Express via req.query:

// URL: /search?q=nodejs&page=2
app.get('/search', (req, res) => {
    const query = req.query.q;     // 'nodejs'
    const page = req.query.page;   // '2' (always a string!)

    res.json({
        query: query,
        page: parseInt(page) || 1
    });
});
Important: Query parameters are always strings! Use parseInt() or Number() to convert to numbers.

POST Form Data (URL-Encoded)

Traditional HTML forms submit data as application/x-www-form-urlencoded. Express needs middleware to parse this:

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

// Enable parsing of URL-encoded form data
app.use(express.urlencoded({ extended: true }));

app.post('/login', (req, res) => {
    const username = req.body.username;
    const password = req.body.password;

    // Validate and process...
    res.json({ message: `Hello, ${username}!` });
});

The HTML form:

<form action="/login" method="POST">
    <input type="text" name="username">
    <input type="password" name="password">
    <button type="submit">Login</button>
</form>

JSON Body (API Requests)

Many APIs accept JSON instead of form-encoded data. Add the JSON middleware:

JSON requires JavaScript: Unlike URL-encoded forms which browsers submit natively, sending JSON requires JavaScript to encode the payload. If you want progressive enhancement (forms that work without JS), use URL-encoded forms. For maximum flexibility, design your endpoints to accept both formats.
// Enable parsing of JSON bodies
app.use(express.json());

app.post('/api/users', (req, res) => {
    const userData = req.body;
    console.log(userData); // { name: 'Alice', email: 'alice@example.com' }

    res.status(201).json({
        message: 'User created',
        user: userData
    });
});

Client-side JavaScript:

fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' })
});

Supporting Both Methods (Progressive Enhancement)

By enabling both middleware, your endpoints can accept either format. This supports progressive enhancement: the form works without JavaScript, but JavaScript clients can send JSON for richer interactions.

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

// Middleware for both types of requests
app.use(express.json());                          // JSON bodies
app.use(express.urlencoded({ extended: true }));  // Form bodies
app.use(express.static('public'));                // Static files

// Now req.body works for both JSON and form data!
// The same endpoint can handle:
// - Traditional form submission (no JS required)
// - fetch() with JSON (JS-enhanced experience)
Progressive Enhancement: Your form works for all users (including those with JavaScript disabled or on slow connections), while JavaScript users get enhanced functionality. This is a best practice for robust web applications.

Complete Form Handling Example

// form-demo.js
const express = require('express');
const app = express();
const PORT = 3000;

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));

// Handle GET request (display search results)
app.get('/search', (req, res) => {
    const query = req.query.q || '';
    res.json({
        type: 'GET',
        searchQuery: query,
        message: query ? `Searching for: ${query}` : 'No query provided'
    });
});

// Handle POST request (form submission)
app.post('/submit', (req, res) => {
    const { username, email, message } = req.body;

    // Basic validation
    if (!username || !email) {
        return res.status(400).json({
            error: 'Username and email are required'
        });
    }

    res.json({
        type: 'POST',
        received: { username, email, message },
        success: true
    });
});

// Handle JSON API request
app.post('/api/data', (req, res) => {
    console.log('Received JSON:', req.body);
    res.json({
        type: 'JSON API',
        received: req.body,
        timestamp: new Date().toISOString()
    });
});

app.listen(PORT, () => {
    console.log(`Form demo running at http://localhost:${PORT}`);
    console.log('Open http://localhost:${PORT}/form.html to test');
});

Input Validation

Never trust user input! Always validate and sanitize:

app.post('/register', (req, res) => {
    const { username, email, age } = req.body;

    // Type checking
    if (typeof username !== 'string') {
        return res.status(400).json({ error: 'Invalid username' });
    }

    // Length validation
    if (username.length < 3 || username.length > 50) {
        return res.status(400).json({ error: 'Username must be 3-50 characters' });
    }

    // Email format (basic check)
    if (!email || !email.includes('@')) {
        return res.status(400).json({ error: 'Invalid email' });
    }

    // Number conversion and range
    const ageNum = parseInt(age);
    if (isNaN(ageNum) || ageNum < 0 || ageNum > 150) {
        return res.status(400).json({ error: 'Invalid age' });
    }

    // All valid!
    res.json({ success: true, username, email, age: ageNum });
});
Production tip: For real applications, use a validation library like express-validator or joi instead of manual validation.

XSS Prevention

When displaying user input in HTML, always escape it to prevent Cross-Site Scripting (XSS) attacks:

// DANGEROUS - Don't do this!
app.get('/greet', (req, res) => {
    res.send(`<h1>Hello, ${req.query.name}!</h1>`);
});
// Attack: /greet?name=<script>alert('hacked')</script>

// SAFE - Escape HTML entities
function escapeHtml(text) {
    return text
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
}

app.get('/greet', (req, res) => {
    const safeName = escapeHtml(req.query.name || 'Guest');
    res.send(`<h1>Hello, ${safeName}!</h1>`);
});
Security: In the PHP tutorial, we discussed how PHP's htmlspecialchars() escapes output. Node.js has no built-in equivalent; you must use a library or function like above. Template engines like EJS or Handlebars escape by default.

Try It Live

Test form handling with our live demo server:

Live Demo: cse135.site:3003/demo

This interactive demo lets you:

View the source: form-live-demo.js

Summary

Task Code
Enable form parsing app.use(express.urlencoded({ extended: true }))
Enable JSON parsing app.use(express.json())
Get query params req.query.paramName
Get POST/JSON body req.body.fieldName
Validate input Check type, length, format before using
Prevent XSS Escape HTML entities or use a template engine