JavaScript and React course Β· Module 11: Data Fetching and APIs
POST, PUT, DELETE - Sending Data to the Server
In this lesson6
So far we have only listened to Mission Control: we fetched data with GET. But the crew also wants to report things: register a new mission, correct its destination or call it off. GET alone can't do that. Now we'll learn how to send data to the server: create new resources (POST), update existing ones (PUT) and delete them (DELETE). It's like sending orders from our spaceship to Mission Control.
HTTP Methods
An HTTP method tells the server what you want to do with the resource at a given address. The same address /missions/7 can mean "show", "change" or "delete", depending on the method:
1// GET - Fetch data (default fetch method)
2// POST - Create a new resource
3// PUT - Update the entire resource
4// PATCH - Update part of a resource
5// DELETE - Delete a resourceThe difference between PUT and PATCH matters: PUT sends the entire new object and replaces the old one with it, PATCH sends only the changed fields. If you leave a field out of a PUT, a server that follows this convention may clear it.
POST Request - Creating New Resources
By default fetch() sends a GET. To send data, we pass a second argument, an options object, with three fields: method, headers and body (the request content):
1async function createMission(missionData) {
2 const response = await fetch(
3 'https://api.space-center.com/missions',
4 {
5 method: 'POST',
6 headers: {
7 'Content-Type': 'application/json',
8 },
9 body: JSON.stringify(missionData),
10 }
11 );
12
13 if (!response.ok) {
14 throw new Error('Failed to create mission');
15 }
16
17 const newMission = await response.json();
18 return newMission;
19}
20
21// Usage:
22const mission = await createMission({
23 name: 'Alpha Centauri Exploration',
24 crew: ['Captain Nova', 'Dr. Astro'],
25 priority: 'high',
26});Why JSON.stringify? The body field accepts text, FormData, Blob and a few other types, but it doesn't serialize a plain JavaScript object. If you passed the object directly, the string "[object Object]" would be sent. Checking response.ok stays the same as with GET, because fetch() doesn't throw for 4xx/5xx with POST either.
Request Headers
The Content-Type header tells the server what format we're sending the data in. Without it the server may not recognize the JSON and reject the request:
1// JSON - the most commonly used format
2headers: {
3 'Content-Type': 'application/json',
4}
5
6// Form data
7headers: {
8 'Content-Type': 'application/x-www-form-urlencoded',
9}
10
11// Authorization - access token
12headers: {
13 'Content-Type': 'application/json',
14 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...',
15}Don't confuse it with the Accept header: that one says what format you want to receive in the response. One trap: when you send FormData (for example with a file), don't set Content-Type by hand, because the browser adds it itself together with the part boundary.
Form with POST in React
In a real app, the data for a POST comes from a form. We need state for the fields, a submitting flag (to disable the button) and state for the result and the error. First, the submit logic:
1import React, { useState } from 'react';
2
3function MissionForm() {
4 const [name, setName] = useState('');
5 const [destination, setDestination] = useState('');
6 const [submitting, setSubmitting] = useState(false);
7 const [result, setResult] = useState(null);
8 const [error, setError] = useState(null);
9
10 const handleSubmit = async (e) => {
11 e.preventDefault();
12 setSubmitting(true);
13 setError(null);
14
15 try {
16 const response = await fetch(
17 'https://api.space-center.com/missions',
18 {
19 method: 'POST',
20 headers: { 'Content-Type': 'application/json' },
21 body: JSON.stringify({ name, destination }),
22 }
23 );
24
25 if (!response.ok) {
26 throw new Error(`Error: ${response.status}`);
27 }
28
29 const data = await response.json();
30 setResult(data);
31 setName('');
32 setDestination('');
33 } catch (err) {
34 setError(err.message);
35 } finally {
36 setSubmitting(false);
37 }
38 };e.preventDefault() stops the form's default page reload. We clear the fields only after success, so on an error the user doesn't lose what they typed. The second part of the same component is the view:
1 return (
2 <form onSubmit={handleSubmit}>
3 <input
4 value={name}
5 onChange={e => setName(e.target.value)}
6 placeholder="Mission name"
7 required
8 />
9 <input
10 value={destination}
11 onChange={e => setDestination(e.target.value)}
12 placeholder="Destination"
13 required
14 />
15 <button type="submit" disabled={submitting}>
16 {submitting ? 'Creating mission...' : 'Create mission'}
17 </button>
18
19 {error && <p className="error">{error}</p>}
20 {result && (
21 <p className="success">
22 Mission "{result.name}" created! ID: {result.id}
23 </p>
24 )}
25 </form>
26 );
27}disabled={submitting} protects against a double click that would send two identical missions. React 19 also added Actions (useActionState, the action prop on <form>), which take over part of this state work, but the HTTP request itself looks exactly the same.
PUT - Updating a Resource
An update looks almost like a create. Two things change: the method becomes PUT and the address now points to a specific mission through its id:
1async function updateMission(id, updatedData) {
2 const response = await fetch(
3 `https://api.space-center.com/missions/${id}`,
4 {
5 method: 'PUT',
6 headers: { 'Content-Type': 'application/json' },
7 body: JSON.stringify(updatedData),
8 }
9 );
10
11 if (!response.ok) {
12 throw new Error('Update failed');
13 }
14
15 return await response.json();
16}The header and JSON.stringify stay the same. Just remember that updatedData should contain the whole mission object.
DELETE - Deleting a Resource
DELETE is the simplest, because it usually sends no content: the method and the address are enough. The server often replies with 204 No Content, meaning "success, but no content", so in that case we don't call response.json():
1async function deleteMission(id) {
2 const response = await fetch(
3 `https://api.space-center.com/missions/${id}`,
4 {
5 method: 'DELETE',
6 }
7 );
8
9 if (!response.ok) {
10 throw new Error('Deletion failed');
11 }
12
13 // DELETE often returns 204 No Content (no body)
14 return response.status === 204;
15}Calling response.json() on an empty 204 response would throw a parse error, which is why we only return information about the status. Now let's connect this function to a button in a component:
1// In a React component
2function MissionItem({ mission, onDelete }) {
3 const [deleting, setDeleting] = useState(false);
4
5 const handleDelete = async () => {
6 if (!window.confirm('Are you sure you want to delete this mission?')) return;
7
8 setDeleting(true);
9 try {
10 await deleteMission(mission.id);
11 onDelete(mission.id);
12 } catch (err) {
13 alert('Error: ' + err.message);
14 } finally {
15 setDeleting(false);
16 }
17 };
18
19 return (
20 <div>
21 <h3>{mission.name}</h3>
22 <button onClick={handleDelete} disabled={deleting}>
23 {deleting ? 'Deleting...' : 'Delete mission'}
24 </button>
25 </div>
26 );
27}The component asks for confirmation because a deletion can't be undone, and the parent updates the list through onDelete only after the server succeeds. I recommend this order to start with. In the caching lesson you'll meet a bolder variant: optimistic updates, which change the UI even before the response.
Remember: GET is listening, while POST, PUT and DELETE are orders sent to Mission Control, each with the right method, header and a response.ok check.
Code for this lesson: App.jsx
1import React, { useState } from 'react';
2
3// API server simulation
4const mockDatabase = {
5 missions: [
6 { id: 1, name: "Alpha Mission", destination: "Mars", status: "active" },
7 { id: 2, name: "Beta Recon", destination: "Europa", status: "active" },
8 { id: 3, name: "Gamma Survey", destination: "Titan", status: "completed" },
9 ],
10 nextId: 4,
11};
12
13function fakeFetchApi(url, options = {}) {
14 return new Promise((resolve, reject) => {
15 setTimeout(() => {
16 const method = (options.method || 'GET').toUpperCase();
17
18 if (method === 'GET') {
19 resolve({
20 ok: true,
21 json: () => Promise.resolve({ missions: [...mockDatabase.missions] }),
22 });
23 } else if (method === 'POST') {
24 const body = JSON.parse(options.body);
25 const newMission = { ...body, id: mockDatabase.nextId++, status: 'active' };
26 mockDatabase.missions.push(newMission);
27 resolve({
28 ok: true,
29 status: 201,
30 json: () => Promise.resolve(newMission),
31 });
32 } else if (method === 'DELETE') {
33 const id = parseInt(url.split('/').pop());
34 mockDatabase.missions = mockDatabase.missions.filter(m => m.id !== id);
35 resolve({ ok: true, status: 204 });
36 }
37 }, 800);
38 });
39}
40
41function MissionControl() {
42 const [missions, setMissions] = useState([]);
43 const [name, setName] = useState('');
44 const [destination, setDestination] = useState('');
45 const [submitting, setSubmitting] = useState(false);
46 const [loading, setLoading] = useState(false);
47 const [message, setMessage] = useState(null);
48
49 const loadMissions = async () => {
50 setLoading(true);
51 const response = await fakeFetchApi('/api/missions');
52 const data = await response.json();
53 setMissions(data.missions);
54 setLoading(false);
55 };
56
57 const createMission = async (e) => {
58 e.preventDefault();
59 if (!name || !destination) return;
60 setSubmitting(true);
61 setMessage(null);
62
63 try {
64 const response = await fakeFetchApi('/api/missions', {
65 method: 'POST',
66 headers: { 'Content-Type': 'application/json' },
67 body: JSON.stringify({ name, destination }),
68 });
69 const newMission = await response.json();
70 setMissions(prev => [...prev, newMission]);
71 setName('');
72 setDestination('');
73 setMessage({ type: 'success', text: `Mission "${newMission.name}" created!` });
74 } catch (err) {
75 setMessage({ type: 'error', text: err.message });
76 } finally {
77 setSubmitting(false);
78 }
79 };
80
81 const deleteMission = async (id) => {
82 try {
83 await fakeFetchApi(`/api/missions/${id}`, { method: 'DELETE' });
84 setMissions(prev => prev.filter(m => m.id !== id));
85 setMessage({ type: 'success', text: 'Mission deleted' });
86 } catch (err) {
87 setMessage({ type: 'error', text: err.message });
88 }
89 };
90
91 React.useEffect(() => { loadMissions(); }, []);
92
93 return (
94 <div className="control">
95 <h1>Mission Command Center</h1>
96
97 <form className="mission-form" onSubmit={createMission}>
98 <h3>New Mission (POST)</h3>
99 <input value={name} onChange={e => setName(e.target.value)} placeholder="Mission name" />
100 <input value={destination} onChange={e => setDestination(e.target.value)} placeholder="Destination" />
101 <button type="submit" disabled={submitting}>
102 {submitting ? 'Creating...' : 'Create mission (POST)'}
103 </button>
104 </form>
105
106 {message && (
107 <div className={"message " + message.type}>{message.text}</div>
108 )}
109
110 <h3>Active missions ({missions.length})</h3>
111 {loading ? <p>Loading...</p> : (
112 <div className="mission-list">
113 {missions.map(m => (
114 <div key={m.id} className="mission-card">
115 <div>
116 <h4>{m.name}</h4>
117 <p>Destination: {m.destination} | Status: {m.status}</p>
118 </div>
119 <button className="delete-btn" onClick={() => deleteMission(m.id)}>
120 Delete (DELETE)
121 </button>
122 </div>
123 ))}
124 </div>
125 )}
126 </div>
127 );
128}
129
130export default MissionControl;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Why do we use JSON.stringify() to pass data in the body of a POST request?
2. Which HTTP header informs the server that we are sending data in JSON format?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Vertical ordering
Arrange the steps of the HTTP request-response cycle:
- Code editor
Form with POST request
- Code editor
CRUD Operations
- Vertical ordering
Arrange the syntax of a fetch call with the POST method and Content-Type header: