Skip to content

Server responses

Your endpoint should return JSON. formsub reads that JSON, decides success vs error, and updates the UI (or calls your callback).

What to return

Success

json
{
  "status": "success",
  "code": 200,
  "message": "Thanks — we got your message."
}

With the default handler, the form is cleared shortly after a success.

Error, with field messages

json
{
  "status": "error",
  "code": 400,
  "message": "Please fix the highlighted fields.",
  "errors": [
    { "name": "email", "error": "Email is already registered" }
  ]
}

Each errors item should include:

  • name or field — matches the HTML name
  • error or message — text shown next to the input

Items may also be plain strings.

How success and error are detected

Success when any of these match:

  • status === "success" or status === 200
  • code === 200 or code === "success" or code === "ok" / "OK"

Error when any of these match:

  • status === "error" or status === 400
  • code === 400 or code === "error" / "ERROR"

Prefer status

Sending both status and code is fine. A single "status": "success" is enough.

If the body is not JSON, or the network fails, formsub still calls your callback / default handler with a synthesized error-shaped object.

Express (Node)

js
import express from 'express';

const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.post('/api/contact', (req, res) => {
  const { email, name } = req.body;

  if (!email) {
    return res.status(400).json({
      status: 'error',
      code: 400,
      message: 'Validation failed',
      errors: [{ name: 'email', error: 'Email is required' }],
    });
  }

  // save message...

  res.json({
    status: 'success',
    code: 200,
    message: 'Message sent!',
  });
});

app.listen(3000);

For file uploads, add middleware such as multer. FormData from the browser is multipart/form-data, not JSON.

Node (native HTTP)

js
import { createServer } from 'node:http';
import { parse } from 'node:querystring';

createServer((req, res) => {
  if (req.method !== 'POST' || req.url !== '/api/contact') {
    res.writeHead(404);
    res.end();
    return;
  }

  let body = '';
  req.on('data', (chunk) => { body += chunk; });
  req.on('end', () => {
    const data = parse(body);
    const json = (status, payload) => {
      res.writeHead(status, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(payload));
    };

    if (!data.email) {
      return json(400, {
        status: 'error',
        code: 400,
        message: 'Validation failed',
        errors: [{ name: 'email', error: 'Email is required' }],
      });
    }

    json(200, {
      status: 'success',
      code: 200,
      message: 'Message sent!',
    });
  });
}).listen(3000);

PHP

php
<?php
header('Content-Type: application/json');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['status' => 'error', 'message' => 'Method not allowed']);
    exit;
}

$email = $_POST['email'] ?? '';
$name  = $_POST['name'] ?? '';

if ($email === '') {
    http_response_code(400);
    echo json_encode([
        'status'  => 'error',
        'code'    => 400,
        'message' => 'Validation failed',
        'errors'  => [['name' => 'email', 'error' => 'Email is required']],
    ]);
    exit;
}

// save message...

echo json_encode([
    'status'  => 'success',
    'code'    => 200,
    'message' => 'Message sent!',
]);

With multipart uploads, text fields are in $_POST and files in $_FILES.

Released under the MIT License.