Form Handling
Forms are how users send data to your server. Express makes it easy to receive and process form data from HTML forms, whether it’s login credentials, search queries, or file uploads.
Form Basics
HTML Form
<form action="/submit" method="POST">
<input type="text" name="username" placeholder="Username">
<input type="password" name="password" placeholder="Password">
<button type="submit">Login</button>
</form>
Express Handler
const express = require('express');
const app = express();
// Required to parse form data
app.use(express.urlencoded({ extended: true }));
app.post('/submit', (req, res) => {
console.log(req.body); // { username: '...', password: '...' }
res.send('Form received!');
});
URL-Encoded Forms
The most common form type (application/x-www-form-urlencoded):
const express = require('express');
const app = express();
// Parse URL-encoded bodies
app.use(express.urlencoded({ extended: true }));
// Serve form
app.get('/', (req, res) => {
res.send(`
<form action="/register" method="POST">
<input type="text" name="username" required>
<input type="email" name="email" required>
<input type="password" name="password" required>
<button>Register</button>
</form>
`);
});
// Handle form submission
app.post('/register', (req, res) => {
const { username, email, password } = req.body;
console.log('New user:', username, email);
res.send(`Welcome, ${username}!`);
});
app.listen(3000);
Extended Option
// extended: false - Use querystring library (simple)
app.use(express.urlencoded({ extended: false }));
// extended: true - Use qs library (supports nested objects)
app.use(express.urlencoded({ extended: true }));
With extended: true:
<input name="user[name]" value="Alice">
<input name="user[age]" value="25">
req.body // { user: { name: 'Alice', age: '25' } }
JSON Forms
Modern apps often send JSON instead of form data:
const express = require('express');
const app = express();
// Parse JSON bodies
app.use(express.json());
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
res.json({
message: 'User created',
user: { name, email }
});
});
app.listen(3000);
JavaScript client:
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Alice',
email: 'alice@example.com'
})
})
.then(res => res.json())
.then(data => console.log(data));
Form Validation
Always validate user input:
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.post('/register', (req, res) => {
const { username, email, password } = req.body;
const errors = [];
// Validation rules
if (!username || username.length < 3) {
errors.push('Username must be at least 3 characters');
}
if (!email || !email.includes('@')) {
errors.push('Valid email is required');
}
if (!password || password.length < 8) {
errors.push('Password must be at least 8 characters');
}
// Check for errors
if (errors.length > 0) {
return res.status(400).json({ errors });
}
// Process valid data
res.json({ message: 'Registration successful!' });
});
Validation Middleware
Reusable validation:
function validateRegistration(req, res, next) {
const { username, email, password } = req.body;
const errors = [];
if (!username?.trim()) {
errors.push('Username is required');
} else if (username.length < 3) {
errors.push('Username too short');
} else if (!/^[a-zA-Z0-9_]+$/.test(username)) {
errors.push('Username can only contain letters, numbers, and underscores');
}
if (!email?.trim()) {
errors.push('Email is required');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push('Invalid email format');
}
if (!password) {
errors.push('Password is required');
} else if (password.length < 8) {
errors.push('Password must be at least 8 characters');
} else if (!/[A-Z]/.test(password)) {
errors.push('Password must contain uppercase letter');
} else if (!/[0-9]/.test(password)) {
errors.push('Password must contain number');
}
if (errors.length > 0) {
return res.status(400).json({ errors });
}
next();
}
app.post('/register', validateRegistration, (req, res) => {
// Data is already validated
res.json({ message: 'Registration successful!' });
});
Different Input Types
Checkboxes
<form action="/preferences" method="POST">
<input type="checkbox" name="newsletter" value="yes"> Newsletter
<input type="checkbox" name="notifications" value="yes"> Notifications
<button>Save</button>
</form>
app.post('/preferences', (req, res) => {
console.log(req.body);
// Checked: { newsletter: 'yes', notifications: 'yes' }
// Unchecked boxes don't appear in req.body!
const newsletter = req.body.newsletter === 'yes';
const notifications = req.body.notifications === 'yes';
res.send('Preferences saved');
});
Multiple Checkboxes (Array)
<form action="/interests" method="POST">
<input type="checkbox" name="interests" value="sports"> Sports
<input type="checkbox" name="interests" value="music"> Music
<input type="checkbox" name="interests" value="tech"> Tech
<button>Save</button>
</form>
app.post('/interests', (req, res) => {
// If multiple checked: array ['sports', 'music']
// If one checked: string 'sports'
// If none checked: undefined
let interests = req.body.interests || [];
// Ensure it's always an array
if (!Array.isArray(interests)) {
interests = [interests];
}
console.log('Selected:', interests);
res.send('Interests saved');
});
Radio Buttons
<form action="/subscribe" method="POST">
<input type="radio" name="plan" value="free" checked> Free
<input type="radio" name="plan" value="pro"> Pro
<input type="radio" name="plan" value="enterprise"> Enterprise
<button>Subscribe</button>
</form>
app.post('/subscribe', (req, res) => {
const plan = req.body.plan; // 'free', 'pro', or 'enterprise'
console.log('Selected plan:', plan);
res.send(`Subscribed to ${plan} plan`);
});
Select Dropdowns
<form action="/settings" method="POST">
<select name="country">
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
</select>
<button>Save</button>
</form>
app.post('/settings', (req, res) => {
const country = req.body.country; // 'us', 'uk', or 'ca'
res.send(`Country set to ${country}`);
});
Text Areas
<form action="/feedback" method="POST">
<textarea name="message" rows="5"></textarea>
<button>Submit</button>
</form>
app.post('/feedback', (req, res) => {
const message = req.body.message;
console.log('Feedback:', message);
res.send('Thank you for your feedback!');
});
Handling Form Responses
Redirect After POST
Prevent duplicate submissions:
app.post('/create-post', (req, res) => {
const { title, content } = req.body;
// Save to database...
// Redirect to avoid resubmission on refresh
res.redirect('/posts');
});
Send HTML Response
app.post('/contact', (req, res) => {
const { name, email, message } = req.body;
// Process contact form...
res.send(`
<!DOCTYPE html>
<html>
<body>
<h1>Thank you, ${name}!</h1>
<p>We'll contact you at ${email} soon.</p>
<a href="/">Back to Home</a>
</body>
</html>
`);
});
JSON Response
For AJAX/Fetch requests:
app.post('/api/contact', (req, res) => {
const { name, email, message } = req.body;
// Validation...
res.json({
success: true,
message: 'Message sent successfully'
});
});
Client-side:
document.querySelector('form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
console.log(result.message);
});
Complete Login Example
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
// Fake user database
const users = [
{ username: 'alice', password: 'secret123' }
];
// Serve login page
app.get('/login', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<style>
form { max-width: 300px; margin: 50px auto; }
input { display: block; width: 100%; margin: 10px 0; padding: 8px; }
button { width: 100%; padding: 10px; background: #007bff; color: white; border: none; }
.error { color: red; margin: 10px 0; }
</style>
</head>
<body>
<form method="POST" action="/login">
<h2>Login</h2>
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
</body>
</html>
`);
});
// Handle login
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Validation
if (!username || !password) {
return res.status(400).send('Username and password required');
}
// Find user
const user = users.find(u => u.username === username);
if (!user || user.password !== password) {
return res.status(401).send(`
<!DOCTYPE html>
<html>
<body>
<div class="error">Invalid credentials</div>
<a href="/login">Try again</a>
</body>
</html>
`);
}
// Success
res.send(`
<!DOCTYPE html>
<html>
<body>
<h1>Welcome, ${username}!</h1>
<p>You are now logged in.</p>
</body>
</html>
`);
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Security Best Practices
Sanitize Input
function sanitize(str) {
// Remove dangerous characters
return str
.trim()
.replace(/[<>]/g, ''); // Remove < and >
}
app.post('/comment', (req, res) => {
const comment = sanitize(req.body.comment);
// Save sanitized comment...
});
Limit Request Size
// Limit to 10kb
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
CSRF Protection
Use tokens to prevent Cross-Site Request Forgery:
// Install: npm install csurf
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.send(`
<form method="POST" action="/submit">
<input type="hidden" name="_csrf" value="${req.csrfToken()}">
<input type="text" name="data">
<button>Submit</button>
</form>
`);
});
app.post('/submit', csrfProtection, (req, res) => {
// Protected against CSRF
res.send('Success');
});
Best Practices
✅ DO:
- Always validate user input
- Sanitize data before saving
- Use HTTPS in production
- Redirect after POST (PRG pattern)
- Limit request body size
- Hash passwords (never store plain text)
❌ DON’T:
- Trust user input
- Return sensitive errors to users
- Store passwords in plain text
- Forget to validate on server (client validation isn’t enough)
- Send passwords in GET requests
Summary
| Task | Code |
|---|---|
| Parse forms | app.use(express.urlencoded({ extended: true })) |
| Parse JSON | app.use(express.json()) |
| Get form data | req.body.fieldName |
| Validate | Check values, return 400 if invalid |
| Redirect | res.redirect('/path') |
| JSON response | res.json({ ... }) |
| Limit size | express.json({ limit: '10kb' }) |
Next Article: Environment Variables