Thuta Learning
ExercisesWeb Developmentintermediate

Practice Set 2 — Data Fetching, Server Actions, and Caching Exercises

Relax. We'll talk through this in plain words — no textbook voice.

What you'll walk away with

  • Practice Practice Set 2 — Data Fetching, Server Actions, and Caching Exercises hands-on
  • Practice the skills you've already learned and cement them
  • Get comfortable finding bugs, fixing them, and checking your own work

Let's think about this for a moment

This set is a step harder than Practice Set 1 and focuses on the data layer and mutation logic. Each task covers a pattern you'd actually run into in production apps — writing API endpoints, form validation, cache invalidation. As you work through these tasks, pay attention to error handling too.

Exercises

(1) Create a route handler at app/api/quotes/route.ts and have its GET method return an array of quotes (text, author) as JSON via Response.json(). (2) Add a POST method that validates the text field of the request body — check it's a string and longer than 3 characters — returning status 400 if it isn't. (3) Build a subscribe form (email input) backed by a "use server" Server Action that validates the email format with a regex and shows a success/error message using useFormState or simple state. (4) After a data mutation, call revalidateTag("quotes") and attach the { next: { tags: ["quotes"] } } option to a fetch() call.

Code Example

typescript
// Task 1-2 skeleton — app/api/quotes/route.ts
const quotes = [{ text: "Stay hungry, stay foolish.", author: "Steve Jobs" }];

export async function GET() {
  return Response.json(quotes);
}

export async function POST(request: Request) {
  const body = await request.json();
  if (typeof body.text !== "string" || body.text.length <= 3) {
    return Response.json({ error: "text is too short" }, { status: 400 });
  }
  quotes.push({ text: body.text, author: body.author ?? "Unknown" });
  return Response.json({ ok: true }, { status: 201 });
}

// Task 3 skeleton — app/actions.ts
"use server";
export async function subscribe(prevState: unknown, formData: FormData) {
  const email = String(formData.get("email") ?? "");
  const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  if (!isValid) return { error: "Invalid email address" };
  return { success: true };
}
You should see
Once all 4 tasks are done, you'll be able to write a full data flow yourself — a backend API endpoint with validation logic, a Server Action with form validation, and tag-based cache invalidation, all working together.

Try it in 5 minutes

Within 5 minutes, test task (2)'s POST endpoint with a short text like curl -X POST -d '{"text":"hi"}' and confirm you get status 400 back.

A quick word of caution

Make sure to set the status code correctly for error cases in your route handler — if it defaults to 200, client-side code will assume success and miss the validation error.

Easy traps

  • Using request.json() inside a POST route handler without await, leaving body as a Promise and breaking the validation logic
  • Assuming calling revalidateTag() alone is enough, while forgetting to attach the tags option to the fetch() call — so the cache never actually invalidates

Now try it yourself

Within 5 minutes, test task (2)'s POST endpoint with a short text like curl -X POST -d '{"text":"hi"}' and confirm you get status 400 back.

You'll know it worked when: Once all 4 tasks are done, you'll be able to write a full data flow yourself — a backend API endpoint with validation logic, a Server Action with form validation, and tag-based cache invalidation, all working together.

Practice Set 2 — Data Fetching, Server Actions, and Caching Exercises | Thuta Learning