javascript-today

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' })