Skip to main content

The full picture

Creating a record involves three parts working together: a FastAPI endpoint, an API client function, and a React form component.
# backend/main.py
class UserCreate(BaseModel):
    name: str
    email: str

@app.post("/api/users", status_code=201)
def create_user(user: UserCreate):
    new_user = {"id": next_id, "name": user.name, "email": user.email}
    users.append(new_user)
    return new_user

The form component

import { useState } from 'react';
import { createUser } from '../api/users';

function CreateUserForm({ onUserCreated }) {
  const [formData, setFormData] = useState({ name: "", email: "" });
  const [error, setError] = useState(null);
  const [submitting, setSubmitting] = useState(false);

  function handleChange(e) {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setError(null);
    setSubmitting(true);

    try {
      const newUser = await createUser(formData);
      onUserCreated(newUser);    // Tell parent about the new user
      setFormData({ name: "", email: "" }); // Reset form
    } catch (err) {
      setError(err.message);
    } finally {
      setSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
        required
      />
      <input
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
        required
      />

      {error && <p className="error">{error}</p>}

      <button type="submit" disabled={submitting}>
        {submitting ? "Creating..." : "Create User"}
      </button>
    </form>
  );
}
Key details:
  • onUserCreated callback — the form doesn’t manage the user list, it just notifies the parent
  • submitting state — disables the button to prevent double submissions
  • Form reset — clears inputs after successful creation
  • Error display — shows the error message from the API

The parent component

The parent owns the user list and passes the callback:
function UserDashboard() {
  const [users, setUsers] = useState([]);

  function handleUserCreated(newUser) {
    setUsers(prev => [...prev, newUser]);
  }

  return (
    <div>
      <h1>Users</h1>
      <CreateUserForm onUserCreated={handleUserCreated} />
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}{user.email}</li>
        ))}
      </ul>
    </div>
  );
}
When the form creates a user, the API returns the full user object (with its new id). The parent adds it to the list. The UI updates instantly — no need to refetch.

Adding validation

Combine frontend validation with backend error handling:
function CreateUserForm({ onUserCreated }) {
  const [formData, setFormData] = useState({ name: "", email: "" });
  const [errors, setErrors] = useState({});
  const [submitting, setSubmitting] = useState(false);

  function validate() {
    const newErrors = {};
    if (!formData.name.trim()) newErrors.name = "Name is required";
    if (!formData.email.trim()) newErrors.email = "Email is required";
    else if (!formData.email.includes("@")) newErrors.email = "Invalid email";
    return newErrors;
  }

  async function handleSubmit(e) {
    e.preventDefault();

    // Frontend validation (instant feedback)
    const validationErrors = validate();
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }

    setErrors({});
    setSubmitting(true);

    try {
      const newUser = await createUser(formData);
      onUserCreated(newUser);
      setFormData({ name: "", email: "" });
    } catch (err) {
      // Backend validation error (e.g., duplicate email)
      if (err.fieldErrors) {
        setErrors(err.fieldErrors);
      } else {
        setErrors({ general: err.message });
      }
    } finally {
      setSubmitting(false);
    }
  }

  function handleChange(e) {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    if (errors[name]) setErrors(prev => ({ ...prev, [name]: null }));
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input name="name" value={formData.name} onChange={handleChange} placeholder="Name" />
        {errors.name && <p className="error">{errors.name}</p>}
      </div>
      <div>
        <input name="email" value={formData.email} onChange={handleChange} placeholder="Email" />
        {errors.email && <p className="error">{errors.email}</p>}
      </div>

      {errors.general && <p className="error">{errors.general}</p>}

      <button type="submit" disabled={submitting}>
        {submitting ? "Creating..." : "Create User"}
      </button>
    </form>
  );
}
Two layers of validation:
  1. Frontend — catches missing fields before sending the request (instant)
  2. Backend — catches things like duplicate emails (requires API call)
Clear field errors when the user starts typing in that field. This gives immediate feedback that they’re fixing the issue. The handleChange function checks if (errors[name]) to do this.

The data flow

1. User fills out form and clicks "Create"
2. Frontend validates → shows errors OR continues
3. fetch() sends POST /api/users with JSON body
4. FastAPI validates with Pydantic → returns 422 OR continues
5. Backend creates record → returns 201 + new user object
6. Frontend receives new user → calls onUserCreated(newUser)
7. Parent adds to state → React re-renders the list
Every step has error handling. The user always knows what happened.

What’s next?

You can create records. Now let’s display them — fetching a list from the API and rendering it in React.

Read operation

Fetch and display data from your FastAPI backend in React components