7. RESTful CRUD API
Follow along with code examples here!
You now have the skills to build a simple server application. But it can only perform one of the four CRUD capabilities.
In this lesson, you will learn how to add create, update, and delete endpoints and best practices for creating a "RESTful API".
Table of Contents:
Essential Questions
By the end of this lesson, you should be able to answer these questions:
What does CRUD stand for? Which HTTP method corresponds to each operation?
What does REST stand for and what are its core principles?
What does
express.json()middleware do? Why is it needed for POST and PATCH requests?What HTTP status codes should you use when a resource is successfully created? When a requested resource is not found?
Why do different HTTP status codes (201, 204, 400, 404) matter for the client?
Key Concepts
RESTful API — an API that conforms to the design principles of representational state transfer (REST).
API Contract — a description of every endpoint an API exposes: the URL, HTTP method, expected request body, and possible responses. Defines the agreement between the server and any client that uses it.
express.json()Middleware — parses JSON data from the request and adds it toreq.bodyin the controller.HTTP Response Codes:
201— Success: Created204— Success: No Content400— Client Error: Invalid Request404— Client Error: Not Found500— Server Error: Internal Server Error503— Server Error: Unavailable
Introduction: Client → Server → Client → Server
Clone down the provided repository and cd into the starter folder. There, you will see a frontend and server provided for you. Run the server and you'll see it serves a simple application that shows a list of fellows and their unique IDs.

This application represents an essential pattern that we've seen a few times now:

This back and forth pattern between a server application and the frontend application (both from the same origin) underlies virtually every web application that you've ever used!
The starter code already has listFellows working on the server and getFellows working on the frontend. The createFellow, updateFellow, and deleteFellow fetch helpers exist as stubs with the wrong URL or missing config — you'll complete them as we go.
What Makes an API RESTful?
Before writing any new endpoints, let's talk about how to design them well.
Pop Quiz: Guess the action that each of these requests will perform!
If you guessed these correctly, there is a good reason why! The endpoints are intuitive to understand thanks to the best practices encouraged by REST (Representational State Transfer).
Core Principles of REST
restfulapi.net puts it best:
REST is an acronym for REpresentational State Transfer... It has become one of the most widely used approaches for building web-based APIs (Application Programming Interfaces).
REST is not a protocol or a standard, it is an architectural style. During the development phase, API developers can implement REST in a variety of ways.
Like the other architectural styles, REST also has its guiding principles and constraints. These principles must be satisfied if a service interface is to be referred to as RESTful.
The guiding principles of REST can be found in detail on that website. Here, we've provided a few easy ways to ensure you are building a RESTful API:
Endpoint URLs Describe Resources, Not Actions: Use plural nouns (e.g.,
/api/users,/api/posts) instead of verbs (e.g.,/api/getUser,/api/createPost) for endpoint URLs.A common exception to this rule is
/api/loginand/api/logout
HTTP Methods Matter: HTTP Methods indicate our desired action. This allows us to performan many different operations on the same endpoint URL (e.g.
GET /api/usersto retrieve users andPOST /api/usersto create a user)GET– Retrieve dataPOST– Create dataPUT / PATCH– Update dataDELETE– Remove data
Endpoint URLs Should Indicate a Clear Hierarchy of Resources:
Use IDs to get individual resources:
/api/users/:userIdNest resources to indicate ownership:
/api/users/:userId/posts/:postIdAvoid deep nesting that becomes hard to manage.
Requests Should be Contain All Information Needed To Complete the Request: This is what we mean when we say that HTTP is a "stateless" protocol. The server doesn't store the current client state between requests. Each request must have all necessary information.
For example, if a user selects a filter that is applied to a
GETrequest, that filter must be provided with every request. The server should not be expected to remember the client's previous requests.
Responses Should Use Proper Status Codes: Here are the most common ones to use:
200 OK
201 Created
400 Bad Request (e.g. they sent a POST request with an empty body)
404 Not Found
500 Internal Server Error (e.g. the third-party API we are using is down)
These principles help our API become "RESTful". But keep in mind that these are just guidelines that help make an API more intuitive and predictable for the client. Providing clear documentation will always be the best way to ensure your API is used properly.
RESTful Principles Quiz
Which of these endpoints below are RESTful? For those that are not, what rule do they break?
GET /api/fellows
✅ RESTful — The URL identifies a resource (the collection of fellows) using a plural noun. GET correctly expresses "retrieve." No verb in the URL, no state stored on the server.
GET /api/getAllFellows
❌ Not RESTful — Verb in the URL (breaks "Endpoints Describe Resources, Not Actions"). The path should describe a resource, not an action. The HTTP method GET already communicates "retrieve" — repeating it in the URL is redundant and non-standard.
✅ Fix: GET /api/fellows
POST /api/fellows
✅ RESTful — POST correctly signals "create a new resource," and /api/fellows identifies the collection that will own it. The URL is a noun, the method is the verb.
POST /api/fellows/3/update
❌ Not RESTful — breaks two rules at once. First, verb in the URL ("update" should not appear in the path). Second, wrong HTTP method — updating an existing resource is the job of PATCH or PUT, not POST.
✅ Fix: PATCH /api/fellows/3
DELETE /api/fellows/7
✅ RESTful — DELETE expresses "remove this resource," and /api/fellows/7 clearly identifies the specific fellow by id. The URL hierarchy (/fellows/:id) follows the REST convention for addressing a single resource within a collection.
GET /api/fellows/delete/5
❌ Not RESTful — breaks two rules. First, verb in the URL ("delete" belongs in the HTTP method, not the path). Second, wrong HTTP method — GET requests are expected to be safe (read-only, no side effects). Using GET to delete data is unpredictable and dangerous; web crawlers and prefetch tools could accidentally trigger deletions.
✅ Fix: DELETE /api/fellows/5
API Contract
Before writing a single line of implementation code, it helps to define the full API contract — the complete list of endpoints your server will expose, what each one expects, and what it returns.
Here is the contract for our fellows tracker:
GET /api/fellows
—
200, array of all fellows
—
GET /api/fellows/:id
—
200, single fellow object
404 if not found
POST /api/fellows
{ fellowName }
201, newly created fellow
400 if name missing
PATCH /api/fellows/:id
{ fellowName }
200, updated fellow
404 if not found
DELETE /api/fellows/:id
—
204, no content
404 if not found
Notice three REST conventions this table demonstrates:
Resource-based URLs — every path describes a thing (
/api/fellows), not an action (/api/getFellows).HTTP methods as verbs —
GET,POST,PATCH, andDELETEexpress the action on that resource.Status codes as communication — the response code tells the client exactly what happened, before they even parse the body.
Why Status Codes Matter
A status code is not just a number — it is a contract between your server and every client that talks to it. Using the right code makes your API predictable and easier to debug.
200 OK— The default success. Use it forGETandPATCHresponses when there is a body to return.201 Created— Use this instead of200forPOSTresponses. It signals "a new resource was created." Some clients and tools (like automated tests) specifically check for201after a create operation.204 No Content— Use this forDELETE. The operation succeeded but there is no body to send back. The204code signals to the client: don't try to parse a body.400 Bad Request— The client sent something invalid (e.g., a missing required field). This is the client's fault.404 Not Found— The requested resource doesn't exist.400and404communicate different problems:400means "your request was malformed," while404means "we understood the request but couldn't find what you asked for."
API Contract Challenge: Design Instagram
Before implementing the API contract above, try designing an API contract for Instagram posts and comments. This planning step will provide a clear vision for how you will implement your API and can even serve as useful instructions when offloading your work to a teammate (or an agentic AI tool).
Your API should have CRUD endpoints for posts and comments and it should follow the rules for making a RESTful API above.
Solution
Notice that we can't use a URL for comments /api/posts/:id/comments/:id because we would have two route parameters called :id. To get around this, we provide more descriptive route parameter names: :postId and :commentId
GET /api/posts
—
200, array of all posts
—
GET /api/posts/:postId
—
200, single post object
404 if not found
POST /api/posts
{ img, content }
201, newly created post
400 if img or content missing
PATCH /api/posts/:postId
{ img, content }
200, updated post
404 if not found
DELETE /api/posts/:postId
—
204, no content
404 if not found
GET /api/posts/:postId/comments
—
200, array of all comments for a post
—
POST /api/posts/:postId/comments
{ content }
201, newly created comment
400 if content missing
PATCH /api/posts/:postId/comments/:commentId
{ content }
200, updated comment
404 if not found
DELETE /api/posts/:postId/comments/:commentId
—
204, no content
404 if not found
Implementing the Endpoints
Now let's implement each endpoint in order, following the API contract above.
GET /api/fellows — Already Working
The starter code already has this endpoint wired up. Let's take a quick look at both sides together:
This pattern — a fetch helper on the frontend that mirrors a controller on the server — is the foundation of every endpoint we'll build.
POST /api/fellows — Creating a New Fellow
To enable our frontend application to create data, we will add a POST endpoint on our server and send a POST request from the frontend, triggered by a form.
Server considerations:
On the server, we need to decide how we expect request bodies to be structured. Let's say we expect an object with the structure
{ fellowName: String }.We will also need to parse this JSON data from incoming requests. With
node:httpwe had to write the code to parse JSON manually but with theexpress.json()middleware, it only takes one line and the data is accessibly inreq.bodyin the controller:
With the built-in node:http package, we have to manually parse the incoming stream of data as chunks of strings and construct the body before parsing it as JSON:
Frontend considerations:
Since
POSTrequests are requests to create data, we need to send data in thebodyof the request. On the frontend, this means including aconfigobject with ourfetchcall.We must also ensure that the structure of our request body matches the structure expected by our server:
With the server controller and the fetch helper both built, we can quickly test by importing the helper and hard-coding a call in main.js:
With this code fully tested, we can now wire it up to our form! See if you can figure out how to trigger the createFellow fetch helper inside the handleAddFellow function.
Solution
When handling the form submission, we use the input value to call createFellow. Then, to update the list with the new data, we call loadFellows() again to re-fetch and re-render.
PATCH /api/fellows/:id — Updating a Fellow
Our frontend application shows an Edit button next to each fellow. The updateFellow fetch helper stub is in the starter code — it just uses the wrong URL and has no config.
Your challenge: implement PATCH on both sides.
Server task: Add an updateFellow controller and register it with app.patch('/api/fellows/:id', updateFellow).
Read
fellowNamefromreq.body— send400if it is missing.Find the fellow by
req.params.id— send404if not found.Update
fellow.nameand send the updated fellow back with200.
Client task: Fix the updateFellow stub in fetch-helpers.js.
Use method
'PATCH', setContent-Type: application/json, and put{ fellowName }in the body.The URL should be
/api/fellows/${id}.
Solution
To wire updateFellow into the UI, we use event delegation — a single click listener on the <ul> that handles all button clicks. When the user clicks "Edit" the row switches to edit mode; when they click "Save" it calls updateFellow:
DELETE /api/fellows/:id — Removing a Fellow
The frontend also shows a Delete button next to each fellow. The deleteFellow fetch helper stub is in the starter code — again with the wrong URL and no config.
Your challenge: implement DELETE on both sides.
Server task: Add a deleteFellow controller and register it with app.delete('/api/fellows/:id', deleteFellow).
Find the fellow's index using
findIndex— send404if the index is-1.Remove the fellow with
spliceand respond withres.sendStatus(204)— no body.
Client task: Fix the deleteFellow stub in fetch-helpers.js.
Use method
'DELETE'.The URL should be
/api/fellows/${id}.There is no response body for a
204— return{ data: true, error: null }on success.
Solution
To wire deleteFellow into the UI, add a delete branch to the same handleFellowsListClick handler we used for update:
Finally, add a catch-all handler after all your routes to return a clean 404 JSON error for any unmatched /api requests:
GET /api/fellows/:id — Completing the Contract
This app always reloads the full list after every mutation, so the frontend never calls this endpoint directly. But it is part of the REST contract — a client that wanted to show a single fellow's detail page would need it.
Recall from lesson 4 that route parameters let us capture values from the URL — req.params.id gives us the :id segment.
Last updated