How to Build a React CRUD App Without Building a Backend

Learn how to build and test a full React CRUD application with persistent mutations, pagination, and filtering without setting up a custom Express backend.

NK
Nilesh Kumar
Creator of Playground API
How to Build a React CRUD App Without Building a Backend

How to Build a React CRUD App Without Building a Backend

Suggested URL Slug: react-crud-without-backend

Primary Keyword: React CRUD without backend

Secondary Keywords: React mock API, stateful frontend prototyping, React CRUD tutorial, frontend data fetching

Meta Description: Learn how to build and test a full React CRUD application with persistent mutations, pagination, and filtering without setting up a custom Express backend.

Suggested Dev.to Tags: #react, #javascript, #webdev, #frontend


You have designed a clean React dashboard component. The state management looks solid, the modal forms are styled, and you are ready to test the full Create, Read, Update, and Delete (CRUD) workflow.

Then you hit the familiar roadblock: there is no backend yet.

To test whether adding a new user updates your list or whether deleting a post handles UI state properly, you are left with two frustrating options:

  1. Spend an entire afternoon spinning up a temporary Node.js/Express server with SQLite or Prisma just to test frontend forms.
  2. Hardcode local mock arrays in React state (useState([ ... ])), which does not test real HTTP network lifecycles, loading states, headers, or error handling.

Building temporary backend scaffolding wastes time that should be spent refining your user interface. In this guide, we will walk through what a modern React CRUD frontend actually requires from an API and how to connect your components to a zero-configuration stateful sandbox API.


What a Frontend CRUD Interface Actually Needs

A functional CRUD application does not care whether the backend is written in Go, Rust, or Node.js. It requires a predictable HTTP contract that supports four fundamental operations:

OperationHTTP MethodEndpoint PatternExpected Payload / Response
Read (List)GET/posts?_page=1&_limit=10Array of items with total count headers
Read (Single)GET/posts/:idSingle resource object
CreatePOST/postsCreated item with generated id
UpdatePUT / PATCH/posts/:idUpdated item payload
DeleteDELETE/posts/:id200 OK or 204 No Content

Beyond standard status codes, a realistic frontend workflow requires:

  1. Network Lifecycle Testing: Simulating isLoading, isError, and isSuccess states.
  2. State Persistence: When a user submits a POST request, navigating back to the list view should display the newly created item.
  3. Query Parameters: Pagination (?_page=1&_limit=5), search queries (?q=keyword), and relational filtering (?user_id=1).

The Limitation of Traditional Static Mock APIs

For years, developers have relied on tools like JSONPlaceholder or static JSON files. While useful for simple GET requests, they fail during CRUD testing:

javascript
1
// Traditional static mock behavior:
2
fetch('https://jsonplaceholder.typicode.com/posts', {
3
method: 'POST',
4
body: JSON.stringify({ title: 'My New Post', body: 'Post content' })
5
})
6
.then(res => res.json())
7
.then(data => console.log(data)); // Returns { id: 101, title: 'My New Post' }
8
9
// But subsequent GET requests do NOT include your new post:
10
fetch('https://jsonplaceholder.typicode.com/posts/101')
11
.then(res => console.log(res.status)); // 404 Not Found!

Because static mock endpoints do not persist mutations, testing pagination, optimistic UI updates, or cache invalidation with tools like TanStack Query or SWR becomes impossible without mocking manual client-side state.


Enter Stateful Mocking with Playground API

To solve this friction, Playground API by Niles Labs provides a free, stateful REST and GraphQL sandbox.

Instead of discarding mutations, Playground API maintains a virtual per-session overlay. When you execute a POST, PUT, PATCH, or DELETE request, the change is saved to your temporary session without altering the shared global seed dataset. Subsequent GET requests immediately reflect your mutations.

Let's build a complete, working React CRUD component using this sandbox.


Building the React CRUD Dashboard

Here is a clean React implementation using standard fetch that handles listing, creating, and deleting posts.

1. API Configuration Module (api.js)

javascript
1
// src/api.js
2
const BASE_URL = 'https://playground.nileslabs.com/api/v1';
3
4
export async function fetchPosts(page = 1, limit = 5) {
5
const response = await fetch(${BASE_URL}/posts?_page=${page}&_limit=${limit});
6
if (!response.ok) throw new Error(HTTP error! status: ${response.status});
7
8
const data = await response.json();
9
const totalCount = response.headers.get('x-total-count') || 100;
10
return { posts: data, totalCount: Number(totalCount) };
11
}
12
13
export async function createPost(postData) {
14
const response = await fetch(${BASE_URL}/posts, {
15
method: 'POST',
16
headers: { 'Content-Type': 'application/json' },
17
body: JSON.stringify(postData),
18
});
19
if (!response.ok) throw new Error('Failed to create post');
20
return response.json();
21
}
22
23
export async function deletePost(id) {
24
const response = await fetch(${BASE_URL}/posts/${id}, {
25
method: 'DELETE',
26
});
27
if (!response.ok) throw new Error('Failed to delete post');
28
return response.json();
29
}

2. The React CRUD Component (PostDashboard.jsx)

jsx
1
// src/components/PostDashboard.jsx
2
import React, { useState, useEffect } from 'react';
3
import { fetchPosts, createPost, deletePost } from '../api';
4
5
export default function PostDashboard() {
6
const [posts, setPosts] = useState([]);
7
const [page, setPage] = useState(1);
8
const [loading, setLoading] = useState(true);
9
const [error, setError] = useState(null);
10
11
// Form state
12
const [title, setTitle] = useState('');
13
const [body, setBody] = useState('');
14
const [submitting, setSubmitting] = useState(false);
15
16
// Load posts on page change
17
const loadPosts = async () => {
18
try {
19
setLoading(true);
20
setError(null);
21
const { posts: data } = await fetchPosts(page, 5);
22
setPosts(data);
23
} catch (err) {
24
setError(err.message);
25
} finally {
26
setLoading(false);
27
}
28
};
29
30
useEffect(() => {
31
loadPosts();
32
}, [page]);
33
34
// Handle Create
35
const handleCreate = async (e) => {
36
e.preventDefault();
37
if (!title.trim() || !body.trim()) return;
38
39
try {
40
setSubmitting(true);
41
await createPost({ title, body, user_id: 1 });
42
setTitle('');
43
setBody('');
44
// Reload posts: your newly created post is now in the list!
45
await loadPosts();
46
} catch (err) {
47
alert(Error creating post: ${err.message});
48
} finally {
49
setSubmitting(false);
50
}
51
};
52
53
// Handle Delete
54
const handleDelete = async (id) => {
55
if (!window.confirm('Delete this post?')) return;
56
try {
57
await deletePost(id);
58
// Remove from list or re-fetch
59
setPosts(current => current.filter(p => p.id !== id));
60
} catch (err) {
61
alert(Error deleting post: ${err.message});
62
}
63
};
64
65
return (
66
<div style={{ maxWidth: '680px', margin: '2rem auto', fontFamily: 'sans-serif' }}>
67
<h2>📝 React CRUD Post Manager</h2>
68
69
{/* Creation Form */}
70
<form onSubmit={handleCreate} style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '2rem' }}>
71
<input
72
type="text"
73
placeholder="Post title..."
74
value={title}
75
onChange={(e) => setTitle(e.target.value)}
76
required
77
/>
78
<textarea
79
placeholder="Post content body..."
80
value={body}
81
onChange={(e) => setBody(e.target.value)}
82
rows={3}
83
required
84
/>
85
<button type="submit" disabled={submitting}>
86
{submitting ? 'Creating...' : '+ Add Post'}
87
</button>
88
</form>
89
90
{/* Status Indicators */}
91
{loading && <p>⏳ Loading posts from API...</p>}
92
{error && <p style={{ color: 'red' }}>❌ Error: {error}</p>}
93
94
{/* Post List */}
95
{!loading && (
96
<div>
97
{posts.map((post) => (
98
<div key={post.id} style={{ border: '1px solid #e2e8f0', padding: '1rem', marginBottom: '1rem', borderRadius: '6px' }}>
99
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
100
<h3 style={{ margin: 0 }}>#{post.id} {post.title}</h3>
101
<button onClick={() => handleDelete(post.id)} style={{ color: '#ef4444' }}>
102
Delete
103
</button>
104
</div>
105
<p style={{ color: '#475569' }}>{post.body}</p>
106
</div>
107
))}
108
109
{/* Pagination Controls */}
110
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center', marginTop: '1.5rem' }}>
111
<button onClick={() => setPage(p => Math.max(p - 1, 1))} disabled={page === 1}>
112
Previous
113
</button>
114
<span>Page {page}</span>
115
<button onClick={() => setPage(p => p + 1)}>
116
Next
117
</button>
118
</div>
119
</div>
120
)}
121
</div>
122
);
123
}

Key Best Practices When Building Without a Backend

  1. Decouple API Calls from Components: Keep API functions in a dedicated service module (api.js or services/posts.js). When your real backend is ready, you only need to change the BASE_URL.
  2. Test Network Latency: Real backends rarely respond in 5ms. Test your UI loading skeletons by passing ?_delay=1000 to simulate realistic network delay.
  3. Verify Error States: Make sure your UI handles HTTP 400, 404, and 500 status codes gracefully rather than crashing.

Limitations & Considerations

While a stateful sandbox is ideal for frontend prototyping, automated UI tests, and staging demos, keep these boundaries in mind:

  • Temporary Persistence: Sandbox records are tied to session cookies or client headers. They are not intended for long-term production storage.
  • No Custom Business Logic: If your application requires complex server-side validation rules or external payment webhooks, you will eventually transition to a production backend.

Conclusion

You don't need to slow down frontend development to build disposable backend servers. By utilizing a stateful mock API with built-in CRUD operations, pagination, and persistent mutations, you can build, iterate, and polish your React interfaces with production-grade data flow on day one.

If you are prototyping a React application and want a ready-to-use stateful backend, check out Playground API by Niles Labs.

Tags:#react#javascript#webdev#frontend
Series: Stop Waiting for the Backend
Part 1 of 12

Try Playground API in Your Own App

Stateful mock REST & GraphQL API with private sandbox overlays.