Imagine you’re using our blogging app. You tap a button to see posts tagged “coding,” write a new post about your latest project, or delete an old draft—all from your browser or phone. How does this happen so seamlessly? The answer is REST APIs—Representational State Transfer Application Programming Interfaces. They’re the digital couriers that shuttle your requests to the server and bring back the data, all over HTTP, the same system that powers the web.
REST APIs are everywhere—think social media feeds, online stores, or even the projects you’ll build in this course. Today, we’ll explore their foundation, how to use them with the fetch
API on the client side, and best practices to make them work for our blogging app. Here’s a fun question: Ever thought an app “knows” what you did last time you used it? With REST, it doesn’t—and that’s a superpower we’ll unpack soon!
Some of you might think REST APIs are just random URLs you click. Not true! They’re a structured way to manage resources—like blog posts—using specific rules and HTTP methods, and we’ll use fetch
to talk to them from the client.
REST APIs are built on simplicity and a few core ideas that make them powerful. Let’s lay the groundwork with our blogging app in mind.
fetch("https://myblog.com/api/posts?tag=coding")
—the server starts fresh each time.https://myblog.com/api/posts/1
is post #1.fetch
to ask for posts; the server doesn’t care what the app looks like.You might think the server remembers your last request. Nope! Statelessness means it forgets after each one—surprising, but it’s why REST handles millions of users so well.
Let’s put REST to work in our blogging app using the fetch
API, a built-in JavaScript tool for making HTTP requests from the client. We’ll use TypeScript for type safety and walk through each HTTP method with complete, working examples.
First, here’s the Post
interface we’ll use across all examples:
interface Post {
id: number;
title: string;
tag: string;
content?: string; // Optional for some operations
}
async function fetchAllPosts(): Promise<Post[]> {
const response = await fetch("https://myblog.com/api/posts");
if (!response.ok) throw new Error("Failed to fetch posts");
const posts: Post[] = await response.json();
return posts;
}
fetchAllPosts()
.then(posts => console.log(posts))
.catch(error => console.error(error));
fetch
sends a GET request to /api/posts
.[{"id": 1, "title": "Learning REST", "tag": "coding"}]
.async function fetchPostsByTag(tag: string): Promise<Post[]> {
const response = await fetch(`https://myblog.com/api/posts?tag=${tag}`);
if (!response.ok) throw new Error(`Failed to fetch ${tag} posts`);
const posts: Post[] = await response.json();
return posts;
}
fetchPostsByTag("coding")
.then(posts => console.log(posts))
.catch(error => console.error(error));
?tag=coding
) to filter results.async function createPost(newPost: Post): Promise<{ id: number; message: string }> {
const response = await fetch("https://myblog.com/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newPost),
});
if (!response.ok) throw new Error("Failed to create post");
const result = await response.json();
return result;
}
const newPost: Post = { id: 0, title: "My API Journey", tag: "coding", content: "So cool!" };
createPost(newPost)
.then(result => console.log(result)) // e.g., { id: 2, message: "Post created" }
.catch(error => console.error(error));
fetch
sends a POST request with a JSON payload.async function updatePost(postId: number, updatedPost: Post): Promise<{ message: string }> {
const response = await fetch(`https://myblog.com/api/posts/${postId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updatedPost),
});
if (!response.ok) throw new Error("Failed to update post");
const result = await response.json();
return result;
}
const updates: Partial<Post> = { title: "Learning REST – Updated" };
updatePost(1, updates)
.then(result => console.log(result)) // e.g., { message: "Post updated" }
.catch(error => console.error(error));
PUT
targets a specific post (/posts/1
) and sends updated fields.async function updatePost(postId: number, updatedPost: Partial<Post>): Promise<{ message: string }> {
const response = await fetch(`https://myblog.com/api/posts/${postId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updatedPost),
});
if (!response.ok) throw new Error("Failed to update post");
const result = await response.json();
return result;
}
const updates: Partial<Post> = { title: "Learning REST – Updated" };
updatePost(1, updates)
.then(result => console.log(result)) // e.g., { message: "Post updated" }
.catch(error => console.error(error));
PATCH
targets a specific post (/posts/1
) and sends updated fields.Partial<Post>
allows flexibility—update only what’s needed.async function deletePost(postId: number): Promise<{ message: string }> {
const response = await fetch(`https://myblog.com/api/posts/${postId}`, {
method: "DELETE",
});
if (!response.ok) throw new Error("Failed to delete post");
const result = await response.json();
return result;
}
deletePost(1)
.then(result => console.log(result)) // e.g., { message: "Post deleted" }
.catch(error => console.error(error));
DELETE
targets a specific post and removes it.Imagine a user flow:
fetchAllPosts()
.fetchPostsByTag("coding")
.createPost()
.updatePost()
.deletePost()
. Each step uses fetch
to talk to the REST API, building a complete app experience.Using REST APIs with fetch
is powerful, but doing it right takes some tips. Let’s explore best practices and pitfalls, tied to our blogging app.
fetch("/api/posts")
, fetch("/api/posts/1")
fetch("/api/getPosts")
(GET is implied!)response.ok
in every fetch
call.if (response.status === 201) console.log("Created!")
.{ title: "New Post" }
in POST.fetch
requests—great for our blog’s growth.fetch
works in browsers and Node.js (with polyfills), talking to any REST API.fetch
calls straightforward.fetch("/api/posts/1")
might return too much or too little.fetch("/api/posts/1?fields=title,tag")
.fetch
repeats auth tokens.fetch(url, { headers: { Authorization: "Bearer token" } })
.fetch
calls.fetch("/api/posts/1?include=comments")
.Filter posts by tag and date:
async function fetchFilteredPosts(tag: string, date: string): Promise<Post[]> {
const response = await fetch(`https://myblog.com/api/posts?tag=${tag}&date=${date}`);
if (!response.ok) throw new Error("Failed to fetch filtered posts");
const posts: Post[] = await response.json();
return posts;
}
fetchFilteredPosts("coding", "2025-04")
.then(posts => console.log(posts))
.catch(error => console.error(error));
fetch
.REST APIs are the glue that connects our blogging app’s client to its server, and fetch
is your tool to make it happen. Here’s what to remember:
fetch
, you can GET posts, POST new ones, PUT updates, and DELETE drafts—all from the client.In our blogging app, fetch
powers everything from browsing posts to managing them as an admin. As you build projects in this course, you’ll use REST APIs and fetch
to create dynamic, real-world apps. Next time you load a webpage or save a post, think: “That’s REST and fetch
at work!”—and now, you can make it work too!
Let's talk about the most common approach to creating APIs and the ways to communicate with them in NextJS.
A REST API—Representational State Transfer Application Programming Interface—lets apps talk to servers using HTTP protocol. It’s how your blogging app fetches posts or saves new ones!
Imagine tapping the coding tag on our blogging app to see tagged posts instantly. REST APIs make that happen by carrying your request to the server and bringing back the data.
You’ll use REST APIs in every web project—social media, shopping apps, even your coursework. Today, we’ll build and use them with Next.js and fetch.
Some think REST is just URLs. Nope! It’s a system for managing resources like blog posts, with rules we’ll explore.
REST is simple but powerful. Here’s what makes it tick:
Each API request stands alone. The server forgets you after replying, so you can not maintain a "conversation" with your API unless you provide all previous data. This is scalable and surprisingly powerful!
REST uses standard methods such as GET, POST, PUT, DELETE—they are same everywhere, so you always know what’s what.
Posts live at a URL such as /api/posts. Each has a unique address!
Unfortunately POST APIs do not pack type safety, but there are some code generation libraries that can help!
REST Requests follow a standard client-server architecture.
First, the client calls the REST Endpoint providing details of what data is requested in URL and message body.
Server responds with the type of data the user has requested, for example plain text, html, json or xml.
Next.js makes APIs easy with file-based routes. Put a route ts file in app/api/posts directory, and it’s an endpoint! This code sets up our blogging app’s API. We use GET to fetch posts—filtered by tag if you add ?tag=coding. The POST adds a new post with an ID.
With Next, you need to export a particular command from the route ts file. To handle GET and POST requests, you need to export functions with that name.
The GET request has no body, and you need to encode all the information in the URL. Think of GET as a simple route visit. Therefore, we parsew the URL of the request and extract the tag from the query string.
Next, we filter current tags based on the tag and return the json value. Using the NextResponse json function also correctly sets the response header type to application/json.
All other non-GET requests, such as POST, and DELETE, have a body. Again, Next comes with a handy helper that reads a json value from the request body, in this case extracting the post we would like to create.
Last, we store the post from the body and return the ID on the new post in a JSON response.
Now let’s use the fetch command to call our API from the client—like a user browsing posts. This example grabs posts tagged “coding” from our Next.js API.
We create a helper function that fetches the data from api posts endpoint, providing the tag we would like to filter on.
When using GET, all we need to provide is the url and potentially the headers we would like to include. In this case, we only specified the url and the tag in the query string.
It is a good practice to check, whether the response is ok. The response is ok when the response code is in 200s, all other requests, such as 404 or 302 are deemed not ok.
We can use the JSON function helper to parse the JSON response. This function is asynchronous.
Last, we return the returned posts.
The helper function is asynchronous, so we need to use promises or async/await approach to read the filtered posts by tag.
This example sends a new post to our API. The server adds an ID and confirms it.
Just so you know, we are using the same API endpoint to save the new post as getting the list of posts.
What changes, is that we specify the the method is POST. When method is not specified GET is assumed.
With POST requests, we need some basic headers, such as the type of content provided in the request's body. In our case, we will be sending JSON data,
Once we set the correct type, we provide the request body, not that the body must always be serialised to string. No other format is accepted.
Once we receive the response, we read the JSON output and return it.
When we use this helper function, note how we expect that ID to be returned. Check out the previous slide to see where the value is coming from!
REST also lets us update (method PUT), patch (method PATCH), or delete (method DELETE) posts—here’s how.
The UPDATE method updates the current post entirely. You should provide whole post to this method.
The PATCH method updates only portions of the current post, and you only need to provide the identifier of the post and the properties you would like to update
Last, the DELETE method deletes the current post.
Let's wrap up with some hints and best practices
Keep your API resource-focused—REST loves simplicity. Do not name your API endpoints by what they do, but by what resources they provide!
Make sure to return the correct codes! This tells fetch that your request worked—next. JS makes this easy.
Always check if the response was ok on your client! It keeps your app from breaking when things go wrong.
REST scales well, as it is stateless!
But watch for overfetching by always returning all the data, such as the whole post, even though you are only interested in post title. You can overfetching with query params like fields=title.