JSON Server Essentials
# What is JSON Server?
JSON Server is a tool that lets you build a mock RESTful API server and DB with a JSON file.
With it, you can develop or test prototypes even when the backend API server or DB is not ready yet.
# Installing and running
Install the json-server package.
Since the mock API server has to run separately from the frontend development server, it is also a good idea to install the concurrently package, which can run scripts at the same time.
1npm i -D json-server concurrently
Once the packages are installed, add scripts to package.json as follows.
With an npm run command, you can run all scripts that start with the watch: prefix at the same time.
1234567{ "scripts": { "watch:json-server": "json-server db.json --port 3000", "watch:vite": "vite", "dev": "concurrently npm:watch:*" } }
Now you can run the frontend development server and the mock API server at the same time with the npm run dev command.
1npm run dev
# Creating the JSON file
Once the package installation is complete, create a db.json file that acts as the database.
Then add the collections you want to manage by hand, as follows.
12345{ "users": [], "posts": [], "comments": [] }
If you think of the JSON file in terms of the structure of MongoDB, a well-known NoSQL database, each top-level array is a collection, each object item in an array is a document, and each property of an object item is a field.
12345678910{ "collection": [ document { "field": "value", "field": "value" }, document, document ] }
First, for practice, you can write JSON data (db.json) like this.
The data has 5 users, 2 posts, and 3 comments, and each post and comment refers to the user who wrote it with userId.
Each comment also refers to the post it belongs to with postId.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768{ "users": [ { "id": "58f2", "name": "Neo", "age": 22 }, { "id": "7a8b", "name": "Lewis", "age": 57 }, { "id": "9c3d", "name": "Evan", "age": 35 }, { "id": "2b1c", "name": "Alex", "age": 22 }, { "id": "4e6a", "name": "Mike", "age": 41 } ], "posts": [ { "id": "47g2", "createdAt": "2025-12-16", "title": "Comparing CSS Grid and Flexbox", "content": "How to decide between CSS Grid and Flex when building a layout. Compares their pros and cons.", "userId": "7a8b" }, { "id": "59h3", "createdAt": "2024-09-29", "title": "Tips for Improving JavaScript Performance", "content": "Introduces JavaScript optimization techniques that improve the performance of web applications.", "userId": "58f2" } ], "comments": [ { "id": "91bg", "createdAt": "2026-02-07", "comment": "Grid and Flex were always hard for me. Thanks for the clear explanation!", "userId": "2b1c", "postId": "47g2" }, { "id": "92ch", "createdAt": "2024-10-01", "comment": "These performance tips are really useful. My app got much faster!", "userId": "2b1c", "postId": "59h3" }, { "id": "93di", "createdAt": "2025-03-14", "comment": "Thanks to this explanation, I think I can manage it well.", "userId": "4e6a", "postId": "59h3" } ] }
If you define the schema of the data above in TypeScript, it looks like this.
The user and post properties are filled in when you use the _embed query parameter.
You can find more about this in 'Fetching data / References'.
12345678910111213141516171819202122export interface User { id: string name: string age: number } export interface Post { id: string createdAt: string title: string content: string userId: string user?: User } export interface Comment { id: string createdAt: string comment: string userId: string postId: string user?: User post?: Post }
# Making requests
Following RESTful principles, JSON Server handles requests per collection.
You write the collection name in the request URL and choose how the request is handled by specifying the request method.
12curl http://localhost:3000/collection \ -X METHOD
12curl http://localhost:3000/users \ -X GET \
123curl http://localhost:3000/users/9c3d \ -X PATCH \ -d '{ "age": 99 }'
You can use the following request methods.
GET: Fetch dataPOST: Create dataPUT: Update all of the dataPATCH: Update part of the dataDELETE: Delete data
# Fetching data
To fetch the user list from the DB, you can write a function like this.
If you add conditions with a query string, you can filter and fetch only the data that matches them.
Without a query string, you fetch all the data in the collection.
12345export async function fetchUsers(query = ''): Promise<User[]> { const res = await fetch(`http://localhost:3000/users/?${query}`) // GET const users = await res.json() return users }
1fetchUsers()
123456789101112131415161718192021222324252627[ { "id": "58f2", "name": "Neo", "age": 22 }, { "id": "7a8b", "name": "Lewis", "age": 57 }, { "id": "9c3d", "name": "Evan", "age": 35 }, { "id": "2b1c", "name": "Alex", "age": 22 }, { "id": "4e6a", "name": "Mike", "age": 41 } ]
# Conditions
You can filter data by adding conditions with a query string.
A name that starts with the _ symbol is treated as a query parameter, not a regular field.
1field_condition=value
_lt: Less Than (<)_lte: Less Than or Equal (<=)_gt: Greater Than (>)_gte: Greater Than or Equal (>=)_ne: Not Equal (!=)
The following example fetches users whose age field (age) is 22, that is, users who are 22 years old.
1fetchUsers('age=22')
123456789101112[ { "id": "58f2", "name": "Neo", "age": 22 }, { "id": "2b1c", "name": "Alex", "age": 22 } ]
The following example fetches users younger than 41.
1fetchUsers('age_lt=41')
1234567891011121314151617[ { "id": "58f2", "name": "Neo", "age": 22 }, { "id": "9c3d", "name": "Evan", "age": 35 }, { "id": "2b1c", "name": "Alex", "age": 22 } ]
The following example fetches users aged 41 or younger.
1fetchUsers('age_lte=41')
12345678910111213141516171819202122[ { "id": "58f2", "name": "Neo", "age": 22 }, { "id": "9c3d", "name": "Evan", "age": 35 }, { "id": "2b1c", "name": "Alex", "age": 22 }, { "id": "4e6a", "name": "Mike", "age": 41 } ]
You can also add other conditions like this.
123fetchUsers('age_gt=41') // Users older than 41 fetchUsers('age_gte=41') // Users aged 41 or older fetchUsers('age_ne=41') // Users who are not 41
# Ranges
You can fetch data by specifying a range with a query string.
You can specify which items to fetch, from where to where, or how many.
_start: Specifies the start index to fetch from._end: Specifies the end index to fetch up to._limit: Specifies how many items to fetch.
The following examples all return the same result.
Like .slice() on arrays, the _end parameter fetches up to, but not including, the end index.
123fetchUsers('_start=0&_end=2') // From index 0 up to, but not including, index 2 fetchUsers('_start=0&_limit=2') // 2 items starting from index 0 fetchUsers('_limit=2') // 2 items starting from index 0
123456789101112[ { "id": "58f2", "name": "Neo", "age": 22 }, { "id": "7a8b", "name": "Lewis", "age": 57 } ]
# Pagination
You can split the data into pages and fetch it one page at a time.
_page: Specifies the page to fetch._per_page: Specifies the number of items per page. The default value is10.
1fetchUsers('_page=2&_per_page=2')
If you check the response, it includes the following page information.
The actual data is in the data property.
first: First page numberprev: Previous page numbernext: Next page numberlast: Last page numberpages: Total number of pagesitems: Total number of itemsdata: Response data
1234567891011121314151617181920{ "first": 1, "prev": 1, "next": 3, "last": 3, "pages": 3, "items": 5, "data": [ { "id": "9c3d", "name": "Evan", "age": 35 }, { "id": "2b1c", "name": "Alex", "age": 22 } ] }
# Sorting
You can specify which field to sort by.
By default, data is sorted in ascending order (asc).
_sort: Specifies the field to sort by.
1fetchUsers('_sort=age')
123456789101112131415161718192021222324252627[ { "id": "58f2", "name": "Neo", "age": 22 }, { "id": "2b1c", "name": "Alex", "age": 22 }, { "id": "9c3d", "name": "Evan", "age": 35 }, { "id": "4e6a", "name": "Mike", "age": 41 }, { "id": "7a8b", "name": "Lewis", "age": 57 } ]
If you want to sort the fetched data in descending order (desc), add the - symbol in front of the field.
1fetchUsers('_sort=-age')
123456789101112131415161718192021222324252627[ { "id": "7a8b", "name": "Lewis", "age": 57 }, { "id": "4e6a", "name": "Mike", "age": 41 }, { "id": "9c3d", "name": "Evan", "age": 35 }, { "id": "58f2", "name": "Neo", "age": 22 }, { "id": "2b1c", "name": "Alex", "age": 22 } ]
You can also sort by multiple fields.
The following sorts by age in descending order, and when the ages are the same, sorts by name in ascending order.
1fetchUsers('_sort=-age,name')
123456789101112131415161718192021222324252627[ { "id": "7a8b", "name": "Lewis", "age": 57 }, { "id": "4e6a", "name": "Mike", "age": 41 }, { "id": "9c3d", "name": "Evan", "age": 35 }, { "id": "2b1c", "name": "Alex", "age": 22 }, { "id": "58f2", "name": "Neo", "age": 22 } ]
# References
This time, let's write a function that fetches the list of comments.
As before, it lets you filter the data with a query string.
12345export async function fetchComments(query = ''): Promise<User[]> { const res = await fetch(`http://localhost:3000/comments/?${query}`) // GET const comments = await res.json() return comments }
If you don't pass a query string, you fetch all comments.
1fetchComments()
1234567891011121314151617181920212223[ { "id": "91bg", "createdAt": "2026-02-07", "comment": "Grid and Flex were always hard for me. Thanks for the clear explanation!", "userId": "2b1c", "postId": "47g2" }, { "id": "92ch", "createdAt": "2024-10-01", "comment": "These performance tips are really useful. My app got much faster!", "userId": "2b1c", "postId": "59h3" }, { "id": "93di", "createdAt": "2025-03-14", "comment": "Thanks to this explanation, I think I can manage it well.", "userId": "4e6a", "postId": "59h3" } ]
With the _embed query parameter, you can fetch the comment list together with each related post, using the post ID (postId).
1fetchComments('_embed=post')
1234567891011121314151617181920212223242526272829303132333435363738394041424344[ { "id": "91bg", "createdAt": "2026-02-07", "comment": "Grid and Flex were always hard for me. Thanks for the clear explanation!", "userId": "2b1c", "postId": "47g2", "post": { "id": "47g2", "createdAt": "2025-12-16", "title": "Comparing CSS Grid and Flexbox", "content": "How to decide between CSS Grid and Flex when building a layout. Compares their pros and cons.", "userId": "7a8b" } }, { "id": "92ch", "createdAt": "2024-10-01", "comment": "These performance tips are really useful. My app got much faster!", "userId": "2b1c", "postId": "59h3", "post": { "id": "59h3", "createdAt": "2024-09-29", "title": "Tips for Improving JavaScript Performance", "content": "Introduces JavaScript optimization techniques that improve the performance of web applications.", "userId": "58f2" } }, { "id": "93di", "createdAt": "2025-03-14", "comment": "Thanks to this explanation, I think I can manage it well.", "userId": "4e6a", "postId": "59h3", "post": { "id": "59h3", "createdAt": "2024-09-29", "title": "Tips for Improving JavaScript Performance", "content": "Introduces JavaScript optimization techniques that improve the performance of web applications.", "userId": "58f2" } } ]
You can also write the _embed query parameter several times to fetch multiple referenced data at once.
1fetchComments('_embed=post&_embed=user')
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859[ { "id": "91bg", "createdAt": "2026-02-07", "comment": "Grid and Flex were always hard for me. Thanks for the clear explanation!", "userId": "2b1c", "postId": "47g2", "post": { "id": "47g2", "createdAt": "2025-12-16", "title": "Comparing CSS Grid and Flexbox", "content": "How to decide between CSS Grid and Flex when building a layout. Compares their pros and cons.", "userId": "7a8b" }, "user": { "id": "2b1c", "name": "Alex", "age": 22 } }, { "id": "92ch", "createdAt": "2024-10-01", "comment": "These performance tips are really useful. My app got much faster!", "userId": "2b1c", "postId": "59h3", "post": { "id": "59h3", "createdAt": "2024-09-29", "title": "Tips for Improving JavaScript Performance", "content": "Introduces JavaScript optimization techniques that improve the performance of web applications.", "userId": "58f2" }, "user": { "id": "2b1c", "name": "Alex", "age": 22 } }, { "id": "93di", "createdAt": "2025-03-14", "comment": "Thanks to this explanation, I think I can manage it well.", "userId": "4e6a", "postId": "59h3", "post": { "id": "59h3", "createdAt": "2024-09-29", "title": "Tips for Improving JavaScript Performance", "content": "Introduces JavaScript optimization techniques that improve the performance of web applications.", "userId": "58f2" }, "user": { "id": "4e6a", "name": "Mike", "age": 41 } } ]
# Adding data
You can add data with a POST request.
The id field of data created by a POST request is generated automatically by JSON Server, so leave the id field out.
12345678export async function createUser(user: Omit<User, 'id'>): Promise<User> { const res = await fetch('http://localhost:3000/users', { method: 'POST', body: JSON.stringify(user) }) const createdUser = await res.json() return createdUser }
1234createUser({ name: 'John', age: 30 })
The response returns the created document.
You can see that it includes the id field.
12345{ "id": "1a2b", "name": "John", "age": 30 }
# Updating data
You can update data with a PUT or PATCH request.
# Full update
A PUT request asks to update the data as a whole.
So you also have to send the fields you are not changing.
You can write a function like the following, which includes the ID of the data to update in the request URL and puts the data to update in the request body.
12345678export async function updateUser(user: User): Promise<User> { const res = await fetch(`http://localhost:3000/users/${user.id}`, { method: 'PUT', body: JSON.stringify(user) }) const updatedUser = await res.json() return updatedUser }
12345updateUser({ id: '58f2', name: 'Neo', age: 123 // Changed })
It returns the updated data.
12345{ "id": "58f2", "name": "Neo", "age": 123 }
# Partial update
A PATCH request asks to update part of the data.
So you don't need to send the fields you are not changing.
///
12345678export async function updateUser(user: Partial<User> & Pick<User, 'id'>): Promise<User> { const res = await fetch(`http://localhost:3000/users/${user.id}`, { method: 'PATCH', body: JSON.stringify(user) }) const updatedUser = await res.json() return updatedUser }
1234updateUser({ id: '58f2', age: 123 // Changed })
12345{ "id": "58f2", "name": "Neo", "age": 123 }
# Deleting data
You can delete specific data with a DELETE request.
You must include the ID of the data to delete in the request URL.
1234567export async function deleteUser(userId: string): Promise<User> { const res = await fetch(`http://localhost:3000/users/${userId}`, { method: 'DELETE' }) const user = await res.json() return user }
1deleteUser('4e6a')
It returns the deleted data.
12345{ "id": "4e6a", "name": "Mike", "age": 41 }
# Bulk delete
To delete multiple items with a single request, you can write a function like this.
123456789export async function deleteUsers(userIds: string[] = []): Promise<User[]> { return Promise.all(userIds.map(async userId => { const res = await fetch(`http://localhost:3000/users/${userId}`, { method: 'DELETE' }) const deletedUser = await res.json() return deletedUser })) }
Pass the IDs of the data to delete as an array.
1deleteUsers(['58f2', '7a8b', '9c3d'])
It returns the list of deleted data.
1234567891011121314151617[ { "id": "58f2", "name": "Neo", "age": 22 }, { "id": "7a8b", "name": "Lewis", "age": 57 }, { "id": "9c3d", "name": "Evan", "age": 35 } ]
# Deleting related data
When you delete data with a DELETE request, you can also delete other data that refers to it.
Just use the _dependent query parameter.
The following function can delete a post together with its related comments.
It deletes all comments in the comments collection whose postId field matches the ID of the deleted post.
12345678export async function deletePost(postId: string, isDeleteRefs = true): Promise<Post> { const query = isDeleteRefs ? '_dependent=comments' : '' const res = await fetch(`http://localhost:3000/posts/${postId}?${query}`, { method: 'DELETE' }) const deletedPost = await res.json() return deletedPost }
The default value of the isDeleteRefs argument is true, so you can omit it when calling the function.
Then both the post and its related comments are deleted.
123// Deletes the post along with its related comments. deletePost('59h3') deletePost('59h3', true)
If you want to delete a post but keep its related comments, pass false as the second argument, as follows.
12// Deletes the post but keeps its related comments. deletePost('59h3', false)
Then, because the post document they referred to is gone, null is assigned to the postId field.
12345678910111213141516171819202122232425{ "comments": [ { "id": "91bg", "createdAt": "2026-02-07", "comment": "Grid and Flex were always hard for me. Thanks for the clear explanation!", "userId": "2b1c", "postId": "47g2" }, { "id": "92ch", "createdAt": "2024-10-01", "comment": "These performance tips are really useful. My app got much faster!", "userId": "2b1c", "postId": null }, { "id": "93di", "createdAt": "2025-03-14", "comment": "Thanks to this explanation, I think I can manage it well.", "userId": "4e6a", "postId": null } ] }
Thanks for reading to the end.
Likes and kind comments help keep this blog going!