7. RESTful CRUD API

circle-info

Follow along with code examples herearrow-up-right!

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:

  1. What does CRUD stand for? Which HTTP method corresponds to each operation?

  2. What does REST stand for and what are its core principles?

  3. What does express.json() middleware do? Why is it needed for POST and PATCH requests?

  4. What HTTP status codes should you use when a resource is successfully created? When a requested resource is not found?

  5. 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 to req.body in the controller.

  • HTTP Response Codes:

    • 201 — Success: Created

    • 204 — Success: No Content

    • 400 — Client Error: Invalid Request

    • 404 — Client Error: Not Found

    • 500 — Server Error: Internal Server Error

    • 503 — 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.

An application that renders a list of fellows fetched from the server

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

A sequence diagram showing the back and forth between a client and a server

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!

chevron-rightGET /api/fellows/3hashtag

Get the fellow with the id 3

chevron-rightPATCH /api/fellows/1hashtag

Update the fellow with the id 1

chevron-rightDELETE /api/fellows/2hashtag

Delete the fellow with the id 2

chevron-rightPOST /api/fellows/2/biohashtag

Create a bio for the fellow with the id 2

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.netarrow-up-right 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:

  1. 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/login and /api/logout

  2. 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/users to retrieve users and POST /api/users to create a user)

    • GET – Retrieve data

    • POST – Create data

    • PUT / PATCH – Update data

    • DELETE – Remove data

  3. Endpoint URLs Should Indicate a Clear Hierarchy of Resources:

    • Use IDs to get individual resources: /api/users/:userId

    • Nest resources to indicate ownership: /api/users/:userId/posts/:postId

    • Avoid deep nesting that becomes hard to manage.

  4. 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 GET request, that filter must be provided with every request. The server should not be expected to remember the client's previous requests.

  5. 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?

chevron-rightGET /api/fellowshashtag

✅ 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.

chevron-rightGET /api/getAllFellowshashtag

❌ 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

chevron-rightPOST /api/fellowshashtag

✅ 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.

chevron-rightPOST /api/fellows/3/updatehashtag

❌ 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

chevron-rightDELETE /api/fellows/7hashtag

✅ 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.

chevron-rightGET /api/fellows/delete/5hashtag

❌ Not RESTful — breaks two rules. First, verb in the URL ("delete" belongs in the HTTP method, not the path). Second, wrong HTTP methodGET 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:

Method + URL
Request Body
Success Response
Error Response

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

circle-info

Notice three REST conventions this table demonstrates:

  1. Resource-based URLs — every path describes a thing (/api/fellows), not an action (/api/getFellows).

  2. HTTP methods as verbsGET, POST, PATCH, and DELETE express the action on that resource.

  3. 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 for GET and PATCH responses when there is a body to return.

  • 201 Created — Use this instead of 200 for POST responses. It signals "a new resource was created." Some clients and tools (like automated tests) specifically check for 201 after a create operation.

  • 204 No Content — Use this for DELETE. The operation succeeded but there is no body to send back. The 204 code 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. 400 and 404 communicate different problems: 400 means "your request was malformed," while 404 means "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.

chevron-rightSolutionhashtag

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

Method + URL
Request Body
Success Response
Error Response

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:http we had to write the code to parse JSON manually but with the express.json() middleware, it only takes one line and the data is accessibly in req.body in the controller:

Frontend considerations:

  • Since POST requests are requests to create data, we need to send data in the body of the request. On the frontend, this means including a config object with our fetch call.

  • 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.

chevron-rightSolutionhashtag

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 fellowName from req.body — send 400 if it is missing.

  • Find the fellow by req.params.id — send 404 if not found.

  • Update fellow.name and send the updated fellow back with 200.

Client task: Fix the updateFellow stub in fetch-helpers.js.

  • Use method 'PATCH', set Content-Type: application/json, and put { fellowName } in the body.

  • The URL should be /api/fellows/${id}.

chevron-rightSolutionhashtag

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 — send 404 if the index is -1.

  • Remove the fellow with splice and respond with res.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.

chevron-rightSolutionhashtag

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