Zero-Code REST API Mocking with json-server for Angular and React

Frontend development often hits a practical wall early: the backend API does not exist yet, is unreachable in local environments, or is shared between multiple teams and cannot be safely modified during development. The typical workarounds - hardcoded response stubs, in-memory interceptors, or hand-rolled Express servers - all impose overhead that does not belong in the development workflow.

json-server offers a different approach. Given a plain JSON file that describes data, it automatically generates a fully functional REST API with filtering, sorting, pagination and relational embedding, served over HTTP, with zero application code required. For Angular and React projects in early development stages, that combination removes a significant amount of friction.

What json-server provides

json-server reads a db.json file and exposes each top-level key as a REST resource. A posts array becomes a full CRUD endpoint at /posts. A profile object becomes a singular resource at /profile. The server handles GET, POST, PUT, PATCH and DELETE requests without any additional configuration.

Version 1 (currently in beta as 1.0.0-beta.x) introduces a few behavioral changes compared to the older v0 series. IDs are now always strings and are auto-generated if missing. The pagination parameter changed from _limit to _per_page. Relationship expansion uses _embed instead of _expand. These differences are relevant when migrating from v0, but for new setups v1 is the correct starting point.

Installation

json-server is available as an npm package. Installing it as a development dependency keeps it out of production bundles:

1npm install --save-dev json-server

It can also be run without a local installation using npx:

1npx json-server db.json

For persistent project use, a local install with a script entry in package.json is the more reliable approach.

Designing the data file

The db.json file acts as both the schema definition and the persisted state of the mock. json-server supports both db.json and db.json5 formats. The JSON Schema reference can be included for editor validation:

 1{
 2  "$schema": "./node_modules/json-server/schema.json",
 3  "users": [
 4    { "id": "1", "name": "Alice Müller", "role": "admin", "active": true },
 5    { "id": "2", "name": "Bob Schneider", "role": "editor", "active": true },
 6    { "id": "3", "name": "Clara Fischer", "role": "viewer", "active": false }
 7  ],
 8  "posts": [
 9    { "id": "1", "title": "Getting Started with Angular", "authorId": "1", "views": 320 },
10    { "id": "2", "title": "React Patterns in 2026", "authorId": "2", "views": 180 },
11    { "id": "3", "title": "TypeScript Deep Dive", "authorId": "1", "views": 450 }
12  ],
13  "comments": [
14    { "id": "1", "text": "Great introduction!", "postId": "1" },
15    { "id": "2", "text": "Very helpful, thanks.", "postId": "1" },
16    { "id": "3", "text": "Looking forward to part 2.", "postId": "2" }
17  ],
18  "settings": {
19    "theme": "dark",
20    "language": "en"
21  }
22}

There is one important design constraint: json-server mutates the db.json file when write requests are handled. This means test writes persist between server restarts unless the file is reset. Committing a clean baseline version of db.json under version control and resetting it before test runs prevents accumulated state from interfering with expected results.

Starting the server

The default command starts the server on port 3000:

1npx json-server db.json

A custom port is available via the --port flag:

1npx json-server db.json --port 3001

The output on startup lists all available resources:

1JSON Server started on PORT :3001
2http://localhost:3001/
3
4Resources
5  http://localhost:3001/users
6  http://localhost:3001/posts
7  http://localhost:3001/comments
8  http://localhost:3001/settings

Available routes

For array resources, json-server generates a complete set of CRUD routes:

1GET    /posts          # list all posts
2GET    /posts/:id      # fetch a single post
3POST   /posts          # create a post (id auto-generated if missing)
4PUT    /posts/:id      # replace a post entirely
5PATCH  /posts/:id      # partial update
6DELETE /posts/:id      # remove a post

For object resources like settings, only read and write operations apply:

1GET   /settings
2PUT   /settings
3PATCH /settings

Querying the API

json-server provides a range of query parameters that cover common frontend scenarios without requiring any custom middleware.

Filtering with conditions

Field-level comparisons use the field:operator=value syntax:

1GET /posts?views:gt=200
2GET /posts?title:contains=angular
3GET /posts?authorId:eq=1
4GET /posts?views:gte=300&authorId:in=1,2

Supported operators include eq, ne, lt, lte, gt, gte, in, contains, startsWith and endsWith. String comparisons are case-insensitive.

Sorting

1GET /posts?_sort=views           # ascending
2GET /posts?_sort=-views          # descending
3GET /posts?_sort=authorId,-views # compound sort

Pagination

1GET /posts?_page=1&_per_page=10

The response wraps the data in a pagination envelope:

1{
2  "first": 1,
3  "prev": null,
4  "next": 2,
5  "last": 3,
6  "pages": 3,
7  "items": 25,
8  "data": [ ]
9}

The _per_page parameter defaults to 10 when not specified. Invalid values are normalized automatically.

1GET /posts?_embed=comments
2GET /comments?_embed=post

Embedding works on the postId foreign key convention. A comments entry with postId: "1" is automatically associated with the posts entry whose id is "1".

Complex filtering

For compound conditions that go beyond simple field equality, _where accepts a JSON object:

1GET /posts?_where={"or":[{"views":{"gt":300}},{"authorId":{"eq":"2"}}]}

Integrating with Angular

In an Angular project, HttpClient connects to json-server the same way it would connect to any REST API. The recommended pattern is to define the base URL in the environment files so switching between the mock and the real API requires changing only one value.

src/environments/environment.ts (development):

1export const environment = {
2  production: false,
3  apiUrl: 'http://localhost:3001/'
4};

src/environments/environment.prod.ts (production):

1export const environment = {
2  production: true,
3  apiUrl: 'https://api.example.com/'
4};

A service that communicates with the mock server:

 1import { Injectable } from '@angular/core';
 2import { HttpClient, HttpParams } from '@angular/common/http';
 3import { Observable } from 'rxjs';
 4import { environment } from '../environments/environment';
 5
 6export interface Post {
 7  id: string;
 8  title: string;
 9  authorId: string;
10  views: number;
11}
12
13@Injectable({ providedIn: 'root' })
14export class PostService {
15  private readonly baseUrl: string = `${environment.apiUrl}/posts`;
16
17  constructor(private readonly http: HttpClient) {}
18
19  getAll(): Observable<Post[]> {
20    return this.http.get<Post[]>(this.baseUrl);
21  }
22
23  getById(id: string): Observable<Post> {
24    return this.http.get<Post>(`${this.baseUrl}/${id}`);
25  }
26
27  getFiltered(minViews: number, page: number): Observable<Post[]> {
28    const params: HttpParams = new HttpParams()
29      .set('views:gte', minViews.toString())
30      .set('_page', page.toString())
31      .set('_per_page', '10');
32
33    return this.http.get<Post[]>(this.baseUrl, { params });
34  }
35
36  create(post: Omit<Post, 'id'>): Observable<Post> {
37    return this.http.post<Post>(this.baseUrl, post);
38  }
39
40  update(id: string, patch: Partial<Post>): Observable<Post> {
41    return this.http.patch<Post>(`${this.baseUrl}/${id}`, patch);
42  }
43
44  remove(id: string): Observable<void> {
45    return this.http.delete<void>(`${this.baseUrl}/${id}`);
46  }
47}

Because the Angular dev server and json-server run on different ports, requests go directly from the browser to localhost:3001. CORS is not an issue in this configuration. However, proxying through Angular’s dev server can be useful when the API base URL must match the frontend origin - for example, when cookie-based sessions are required.

A proxy configuration at proxy.conf.json:

1{
2  "/api": {
3    "target": "http://localhost:3001/",
4    "secure": false,
5    "pathRewrite": { "^/api": "" }
6  }
7}

Register the proxy file in angular.json under projects.<name>.architect.serve.options.proxyConfig.

Integrating with React

React does not prescribe a specific HTTP client. The examples below use the native fetch API, but the same patterns apply with axios or any other library.

Centralizing the API base URL using a Vite environment variable:

1// src/config/api.ts
2export const API_BASE_URL: string =
3  import.meta.env.VITE_API_URL ?? 'http://localhost:3001/';

A typed service module for posts:

 1// src/services/postService.ts
 2import { API_BASE_URL } from '../config/api';
 3
 4export interface Post {
 5  id: string;
 6  title: string;
 7  authorId: string;
 8  views: number;
 9}
10
11export interface PagedResponse<T> {
12  first: number;
13  prev: number | null;
14  next: number | null;
15  last: number;
16  pages: number;
17  items: number;
18  data: T[];
19}
20
21export async function fetchPosts(): Promise<Post[]> {
22  const response: Response = await fetch(`${API_BASE_URL}/posts`);
23  if (!response.ok) {
24    throw new Error(`Failed to fetch posts: ${response.status}`);
25  }
26  return response.json() as Promise<Post[]>;
27}
28
29export async function fetchPostsPaged(
30  page: number,
31  perPage: number = 10
32): Promise<PagedResponse<Post>> {
33  const url: URL = new URL(`${API_BASE_URL}/posts`);
34  url.searchParams.set('_page', page.toString());
35  url.searchParams.set('_per_page', perPage.toString());
36
37  const response: Response = await fetch(url.toString());
38  if (!response.ok) {
39    throw new Error(`Failed to fetch posts: ${response.status}`);
40  }
41  return response.json() as Promise<PagedResponse<Post>>;
42}
43
44export async function createPost(post: Omit<Post, 'id'>): Promise<Post> {
45  const response: Response = await fetch(`${API_BASE_URL}/posts`, {
46    method: 'POST',
47    headers: { 'Content-Type': 'application/json' },
48    body: JSON.stringify(post),
49  });
50  if (!response.ok) {
51    throw new Error(`Failed to create post: ${response.status}`);
52  }
53  return response.json() as Promise<Post>;
54}

Using the service in a component:

 1// src/components/PostList.tsx
 2import { useEffect, useState } from 'react';
 3import { fetchPosts, type Post } from '../services/postService';
 4
 5export function PostList() {
 6  const [posts, setPosts] = useState<Post[]>([]);
 7  const [loading, setLoading] = useState<boolean>(true);
 8  const [error, setError] = useState<string | null>(null);
 9
10  useEffect(() => {
11    let cancelled: boolean = false;
12
13    fetchPosts()
14      .then((data: Post[]) => {
15        if (!cancelled) {
16          setPosts(data);
17        }
18      })
19      .catch((err: Error) => {
20        if (!cancelled) {
21          setError(err.message);
22        }
23      })
24      .finally(() => {
25        if (!cancelled) {
26          setLoading(false);
27        }
28      });
29
30    return () => {
31      cancelled = true;
32    };
33  }, []);
34
35  if (loading) return <p>Loading...</p>;
36  if (error) return <p>Error: {error}</p>;
37
38  return (
39    <ul>
40      {posts.map((post: Post) => (
41        <li key={post.id}>
42          {post.title} ({post.views} views)
43        </li>
44      ))}
45    </ul>
46  );
47}

The cancelled flag prevents state updates after the component has unmounted, which avoids a common source of memory leak warnings in React.

For React projects using Vite, CORS can be avoided entirely by configuring a dev server proxy in vite.config.ts:

 1import { defineConfig } from 'vite';
 2import react from '@vitejs/plugin-react';
 3
 4export default defineConfig({
 5  plugins: [react()],
 6  server: {
 7    proxy: {
 8      '/api': {
 9        target: 'http://localhost:3001/',
10        changeOrigin: true,
11        rewrite: (path: string) => path.replace(/^\/api/, ''),
12      },
13    },
14  },
15});

With this configuration, the React app calls /api/posts and Vite routes the request to http://localhost:3001/posts. The environment variable VITE_API_URL can then be set to /api for local development.

Running both servers in parallel

A typical development workflow requires two processes: the mock server and the framework dev server. The concurrently package handles this cleanly:

1npm install --save-dev concurrently

An example package.json scripts section for a React project:

1{
2  "scripts": {
3    "mock": "json-server --port 3001 db.json",
4    "dev": "vite",
5    "start": "concurrently \"npm run mock\" \"npm run dev\""
6  }
7}

concurrently prefixes output from each process and terminates all processes when one exits, which avoids orphaned json-server instances after stopping the dev session.

For Angular, the equivalent would replace vite with ng serve:

1{
2  "scripts": {
3    "mock": "json-server --port 3001 db.json",
4    "start": "concurrently \"npm run mock\" \"ng serve\""
5  }
6}

Limitations and when json-server is not enough

json-server is well-suited for prototyping, onboarding and early-phase feature development, but it has hard constraints that become relevant as projects grow.

No authentication or authorization. json-server serves all data without any access control. It is not a replacement for a secured staging API and should never be exposed beyond localhost.

File-based persistence with mutation risk. Because state is stored directly in db.json, concurrent writes during parallel test execution can corrupt the file. Resetting db.json to a clean baseline before each test run is the only reliable mitigation.

No business logic. Derived fields, validation rules, state transitions and side effects are not supported. Any scenario requiring more than pure data storage calls for a different tool - a custom Express or Fastify server, or a contract-testing approach with tools like Pact.

Memory-bounded performance. The entire db.json file is loaded into memory on each write operation. Performance degrades noticeably with large files.

For projects that reach these limits, a lightweight custom server provides more control while remaining close to json-server’s simplicity in setup cost.

Summary

json-server provides a practical, low-friction starting point for frontend API mocking. A plain JSON file is enough to expose a REST API with full CRUD support, filtering, sorting, pagination and embedded relationships - no code required. For Angular, the integration fits naturally into the HttpClient and environment file pattern. For React, a typed service module combined with a Vite proxy produces clean component code without CORS concerns.

The tool works best at the beginning of a project or during isolated feature development. Understanding its limitations from the start avoids the common mistake of relying on it beyond its appropriate scope.


Let's Work Together

Looking for an experienced Platform Architect or Engineer for your next project? Whether it's cloud migration, platform modernization or building new solutions from scratch - I'm here to help you succeed.

New Platforms
Modernization
Training & Consulting

Comments

Twitter Facebook LinkedIn WhatsApp