Topic 5: JSON Data
📖 7 min read · 🎯 intermediate · 🧭 Prerequisites: building-a-http-server-with-node-js-using-http-apis, joint-query-nested-query-filtering-data
Why this matters
Up until now, you've been writing JavaScript that mostly works with numbers and strings — values that exist in your code and stay there. But real apps need to move data around: from a file, to a server, to a browser. Here's the thing — every piece of that journey speaks the same language, and that language is JSON. Your Node server reads it, your Express API sends it, your React frontend displays it. Once you understand how JSON is structured and how to work with it in JavaScript, the whole stack starts to feel connected instead of like three separate things you're juggling at once.
What You'll Learn
- Read and write JSON files in Node.js using the
fsmodule - Build a full CRUD REST API in Express that consumes and produces JSON
- Test Express endpoints with Postman
- Fetch JSON data from an API inside a React component using Axios
The Analogy
Think of JSON as a universal shipping label. No matter whether a package travels from a warehouse (your database), through a sorting facility (your Express server), or lands on a customer's doorstep (your React UI), every stop along the route can read exactly the same label format — sender, contents, weight, destination. The label doesn't care what language the warehouse clerk speaks or what OS the sorting machine runs; the structure is the contract. JSON works the same way: a lightweight, human-readable text format that every modern language can parse and generate, making it the universal envelope for data on the web.
Chapter 1: Introduction to JSON
JSON — JavaScript Object Notation — is a text format for representing structured data based on JavaScript object syntax. Despite its name it is language-agnostic; virtually every programming environment has a parser for it. It is the default data interchange format for web APIs, configuration files, and inter-service communication.
JSON supports six value types:
- String —
"hello" - Number —
42or3.14 - Boolean —
true/false - Null —
null - Array — ordered list:
[1, 2, 3] - Object — key/value map:
{"key": "value"}
A typical JSON document looks like this:
{
"name": "Alice",
"age": 30,
"email": "alice@example.com",
"skills": ["JavaScript", "Node.js", "React"]
}
Every key must be a double-quoted string. Values follow the type rules above. Trailing commas are not allowed — a common gotcha.
Chapter 2: Reading and Writing JSON Files
Node.js ships with the fs (file system) module, which lets you read and write files on disk. Combined with the built-in JSON.parse() and JSON.stringify() methods, you have everything you need to persist JSON data locally.
Step 1: Reading a JSON File
readJsonFile.js:
const fs = require('fs');
fs.readFile('data.json', 'utf8', (err, data) => {
if (err) {
console.error('Error reading file:', err);
return;
}
const jsonData = JSON.parse(data);
console.log('JSON data:', jsonData);
});
fs.readFile is asynchronous — it hands you the raw file string via callback. JSON.parse() converts that string into a live JavaScript object you can work with.
Run it:
node readJsonFile.js
Step 2: Writing to a JSON File
writeJsonFile.js:
const fs = require('fs');
const jsonData = {
name: 'Bob',
age: 25,
email: 'bob@example.com',
skills: ['Python', 'Django', 'Flask']
};
fs.writeFile('output.json', JSON.stringify(jsonData, null, 2), 'utf8', (err) => {
if (err) {
console.error('Error writing file:', err);
return;
}
console.log('File has been saved.');
});
JSON.stringify(jsonData, null, 2) serializes the object back into a string. The third argument 2 is the indent size — omit it and you get a single-line blob; include it and you get a readable, pretty-printed file.
Run it:
node writeJsonFile.js
After running, inspect output.json — you should see a neatly indented file with Bob's details.
Chapter 3: Using JSON with Express
Express makes serving and consuming JSON trivial. The key is the express.json() middleware, which automatically parses incoming Content-Type: application/json request bodies and attaches the result to req.body.
Step 1: Set Up an Express Application
server.js:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware to parse JSON data
app.use(express.json());
// Sample data
let users = [
{ id: 1, name: 'Alice', age: 30, email: 'alice@example.com' },
{ id: 2, name: 'Bob', age: 25, email: 'bob@example.com' }
];
// GET endpoint to return all users
app.get('/users', (req, res) => {
res.json(users);
});
// POST endpoint to add a new user
app.post('/users', (req, res) => {
const newUser = req.body;
newUser.id = users.length + 1;
users.push(newUser);
res.status(201).json(newUser);
});
// PUT endpoint to update a user
app.put('/users/:id', (req, res) => {
const userId = parseInt(req.params.id);
const updatedUser = req.body;
const userIndex = users.findIndex(user => user.id === userId);
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' });
}
users[userIndex] = { id: userId, ...updatedUser };
res.json(users[userIndex]);
});
// DELETE endpoint to delete a user
app.delete('/users/:id', (req, res) => {
const userId = parseInt(req.params.id);
users = users.filter(user => user.id !== userId);
res.status(204).send();
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Run the server:
node server.js
The four routes form a complete CRUD surface:
| Method | Route | Action |
|---|---|---|
| GET | /users | Return all users |
| POST | /users | Add a new user |
| PUT | /users/:id | Replace a user by ID |
| DELETE | /users/:id | Remove a user by ID |
Step 2: Testing the Endpoints with Postman
Open Postman and fire each request in turn.
1. GET /users
- URL:
http://localhost:3000/users - Method:
GET - Send the request to retrieve all users.
2. POST /users
- URL:
http://localhost:3000/users - Method:
POST - Body (JSON):
{
"name": "Charlie",
"age": 28,
"email": "charlie@example.com"
}
- Send the request to add a new user.
3. PUT /users/:id
- URL:
http://localhost:3000/users/1 - Method:
PUT - Body (JSON):
{
"name": "Alice Updated",
"age": 31,
"email": "alice.updated@example.com"
}
- Send the request to update the user with ID 1.
4. DELETE /users/:id
- URL:
http://localhost:3000/users/2 - Method:
DELETE - Send the request to delete the user with ID 2.
sequenceDiagram
participant Client as Postman / React
participant Express as Express Server
participant Store as In-Memory Users Array
Client->>Express: GET /users
Express->>Store: read users[]
Store-->>Express: [Alice, Bob]
Express-->>Client: 200 JSON array
Client->>Express: POST /users {Charlie}
Express->>Store: push new user
Store-->>Express: updated array
Express-->>Client: 201 JSON {id:3, ...}
Client->>Express: PUT /users/1 {Alice Updated}
Express->>Store: findIndex + replace
Store-->>Express: updated user
Express-->>Client: 200 JSON updated user
Client->>Express: DELETE /users/2
Express->>Store: filter out id=2
Express-->>Client: 204 No Content
Chapter 4: Fetching JSON Data in a React Application
With the Express server running, the next step is consuming that JSON from a React frontend. The class used Axios — a promise-based HTTP client that handles JSON serialization and deserialization automatically.
Step 1: Create a New React Project
npx create-react-app json-data-demo
cd json-data-demo
npm install axios
Step 2: Fetch Data in a Component
src/components/Users.js:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
axios.get('http://localhost:3000/users')
.then((response) => {
setUsers(response.data);
setLoading(false);
})
.catch((error) => {
setError(error);
setLoading(false);
});
}, []);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
export default Users;
The empty dependency array [] on useEffect means the fetch runs once on mount. response.data is already a parsed JavaScript object — Axios handles JSON.parse for you.
Step 3: Integrate the Component into the App
src/App.js:
import React from 'react';
import './App.css';
import Users from './components/Users';
function App() {
return (
<div className="App">
<header className="App-header">
<h1>JSON Data Demo</h1>
</header>
<Users />
</div>
);
}
export default App;
Step 4: Run the React Application
npm start
Navigating to http://localhost:3000 in a web browser displays the list of users fetched from the Express API. Make sure the Express server (node server.js) is still running in a separate terminal when you start the React app.
🧪 Try It Yourself
Task: Add a POST form to the React app that lets you create a new user and immediately see them appear in the list.
- Add a form with three inputs —
name,age,email— and a Submit button toUsers.js. - On submit, call
axios.post('http://localhost:3000/users', formData). - On a successful response, append
response.datato theusersstate array.
Success criterion: After you fill in the form and click Submit, the new user's name and email appear at the bottom of the list — no page refresh needed.
Starter snippet (add inside the Users component):
const [form, setForm] = useState({ name: '', age: '', email: '' });
const handleSubmit = (e) => {
e.preventDefault();
axios.post('http://localhost:3000/users', form)
.then((response) => {
setUsers((prev) => [...prev, response.data]);
setForm({ name: '', age: '', email: '' });
});
};
// In your JSX:
// <form onSubmit={handleSubmit}>
// <input value={form.name} onChange={e => setForm({...form, name: e.target.value})} placeholder="Name" />
// <input value={form.age} onChange={e => setForm({...form, age: e.target.value})} placeholder="Age" />
// <input value={form.email} onChange={e => setForm({...form, email: e.target.value})} placeholder="Email" />
// <button type="submit">Add User</button>
// </form>
🔍 Checkpoint Quiz
Q1. What does the third argument 2 do in JSON.stringify(data, null, 2)?
A) It limits the output to 2 levels of nesting
B) It sets the indentation to 2 spaces, producing pretty-printed output
C) It tells stringify to skip the first 2 keys
D) It converts numbers to 2 decimal places
Q2. Given this Express route, what HTTP status code is returned when a user with the requested ID does not exist?
app.put('/users/:id', (req, res) => {
const userIndex = users.findIndex(user => user.id === parseInt(req.params.id));
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' });
}
users[userIndex] = { id: parseInt(req.params.id), ...req.body };
res.json(users[userIndex]);
});
A) 200
B) 201
C) 404
D) 500
Q3. In the React Users component, why is the dependency array of useEffect left empty ([])?
A) It causes the effect to re-run every time any state changes
B) It prevents the component from rendering
C) It makes the fetch run exactly once when the component first mounts
D) It disables the loading state
Q4. You have a Node.js app that reads config.json with fs.readFile. A teammate reports that the parsed object always shows outdated values even after they edited the file. What is the most likely cause, and how would you confirm it?
A1. B — The third argument to JSON.stringify is the space parameter. Passing 2 inserts two-space indentation at each nesting level, making the output human-readable.
A2. C — The route explicitly calls res.status(404).json({ error: 'User not found' }) when findIndex returns -1, which means no match was found.
A3. C — An empty dependency array tells React "this effect has no dependencies that change," so it fires once after the initial render (mount) and never again. This is the standard pattern for a one-time data fetch on component load.
A4. The most likely cause is that require('fs') is not the issue — but if they switched to require('./config.json'), Node.js caches that at startup and never re-reads the file. Confirm by logging data immediately after fs.readFile — if readFile is used correctly, the latest file contents will appear. If require is being used instead, replace it with fs.readFile + JSON.parse to bypass the module cache.
🪞 Recap
- JSON is a text-based, language-agnostic data format built on JavaScript object syntax, supporting strings, numbers, booleans, null, arrays, and objects.
fs.readFile+JSON.parsereads a JSON file into a live JS object;JSON.stringify+fs.writeFilepersists it back to disk.- The
express.json()middleware must be registered before any route that readsreq.bodyfrom a JSON request. - A four-route Express API (GET, POST, PUT, DELETE) gives you a complete CRUD surface over JSON data.
- Axios in React automatically deserializes JSON responses into
response.data, pairing cleanly withuseStateanduseEffectfor async data fetching.
📚 Further Reading
- JSON specification (json.org) — the source of truth on valid JSON syntax
- Node.js
fsmodule docs — full reference forreadFile,writeFile, and their promise-based equivalents - Express
res.json()docs — how Express serializes and sets Content-Type headers automatically - Axios documentation — full guide to request config, interceptors, and error handling
- ⬅️ Previous: Joint Query, Nested Query & Filtering Data
- ➡️ Next: State and Props