Fetching Order Details

Fetching Order Details

Welcome back. In the previous lesson, Shopping Cart and Checkout, you completed the main purchase flow of the storefront. Shoppers could choose quantities, add products to the cart, review line items, apply a tax country, and finish checkout. At the end of that flow, the app redirected them to a success route.

That redirect was an important milestone, but the success experience still needs one more piece to feel complete: it should show the actual order that was created, not just a generic “thanks” message. In this lesson, you will build the final data path that powers that confirmation screen. You will add a focused API helper for reading one order, wrap that request inside a reusable hook, and use the hook in a dedicated success-page client component.

The central pattern here should feel familiar by now. The API layer exposes a small semantic helper. A hook hides the async mechanics and returns { data, loading, error } style state. And the UI component consumes that stable hook interface to branch cleanly between loading, error, empty, and success states. This is the same architecture you have been building throughout the course, and it is exactly what makes the storefront easier to read and extend.

Previously: Checkout Flow and Success Routing

In the previous lesson, the cart page completed checkout through the shared useCart() context and then redirected to /checkout/success/[id]. That meant the route already had access to the new order ID, and the shopper had a clear transition from cart review into a confirmation screen.

This lesson builds directly on that foundation. Instead of keeping the success page generic, you will now use that route parameter to load the specific order that was created. That makes the confirmation screen feel more trustworthy and complete, because the shopper can immediately see which order exists and what total was charged.

Adding a Focused Order API Helper

The first file in this lesson is src/lib/api/orders.ts. This file is intentionally tiny, and that is a good thing. The goal here is not to build a massive order SDK. The goal is simply to give the success page a clean, semantic helper for reading one order by ID.

Here is the complete file:

import { apiRequest } from '@/lib/api/client';
import { Order } from '@/types/domain';
  • apiRequest is the shared low-level client helper you built earlier in the course. It already knows how to call fetch, parse JSON, understand the API envelope, and turn backend failures into a consistent error shape.

  • Order is the shared domain type that describes what an order looks like in the frontend. Importing that type here lets the helper communicate clearly what kind of data the UI should expect back.

Now look at the actual helper:

export function getOrder(id: string) {
  return apiRequest<Order>(`/api/orders/${id}`);
}
  • getOrder(id) is a very small helper, but it is doing important architectural work. Components and hooks should not build endpoint strings inline when the app can provide a meaningful helper like getOrder() instead.

  • The generic apiRequest<Order>() tells TypeScript that this request resolves to a typed Order. That means any code calling getOrder() will get proper autocomplete and type safety for fields like id, status, total_cents, and currency.

  • The URL uses a template literal so the provided order ID becomes part of the path. This follows the same dynamic-path pattern you have already seen in product and cart helpers.

  • Keeping the file this small reduces noise and makes the data layer easier to understand at a glance. When a route only needs one action, a small focused module is often a sign of good design, not missing sophistication.

This helper is part of the frontend data layer, not a visual feature. Its only job is to give the UI a trustworthy way to request one order.

Building a Reusable useOrder Hook

Next, the success page needs a hook that loads one order and hides the async mechanics. The file src/lib/hooks/useOrder.ts plays the same role for orders that useProduct() played for product details.

The consuming component should not need to know about refs, effect orchestration, or stale request protection. It should be able to think in terms of data, loading, and error.

At the top of the file, the imports and state setup establish that shape:

import { useEffect, useRef, useState } from 'react';
import { getErrorMessage } from '@/lib/api/client';
import { getOrder } from '@/lib/api/orders';
import { Order } from '@/types/domain';

export function useOrder(orderId: string) {
  const [order, setOrder] = useState<Order | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const requestIdRef = useRef(0);
  • order, isLoading, and errorMessage are the three core state values the success page needs. Together, they are enough for the UI to express every important branch: loading, explicit failure, missing data, or successful confirmation.

  • order starts as null because nothing has been fetched yet. Once the request succeeds, it becomes the current order snapshot.

  • isLoading begins as true, which allows the consuming screen to render a loading state immediately while the first request is in flight.

  • errorMessage stores a user-facing failure message instead of a raw thrown error. This keeps the UI simpler because it does not need to inspect multiple error types itself.

  • requestIdRef is the stale-request guard pattern used elsewhere in the course. Even though the success page usually only loads once, keeping the hook aligned with the same async pattern makes the code more consistent and more robust.

Now look at the effect that performs the actual loading:

  useEffect(() => {
    async function loadOrder() {
      const requestId = requestIdRef.current + 1;
      requestIdRef.current = requestId;
      setIsLoading(true);

      try {
        const response = await getOrder(orderId);
        if (requestId !== requestIdRef.current) return;
        setOrder(response);
        setErrorMessage(null);
      } catch (error) {
        if (requestId !== requestIdRef.current) return;
        setOrder(null);
        setErrorMessage(getErrorMessage(error));
      } finally {
        if (requestId === requestIdRef.current) {
          setIsLoading(false);
        }
      }
    }

    void loadOrder();
  }, [orderId]);
  • useEffect(..., [orderId]) makes the hook react whenever the incoming orderId changes. That means the hook stays correct even if it is ever reused in a screen where the requested order changes dynamically.

  • Inside the effect, loadOrder() creates a new requestId and stores it in requestIdRef. This gives each request a unique sequence value so the hook can tell whether a completed response is still the most recent one.

  • setIsLoading(true) runs before the request begins so the UI can immediately show a loading state.

  • await getOrder(orderId) uses the focused API helper instead of embedding a fetch call inside the hook. This keeps the hook aligned with the rest of the course architecture: hooks consume semantic data helpers, not raw transport details.

  • On success, the hook stores the fetched order and clears any old error message. Clearing previous error state matters because a new successful request should not leave an outdated failure message on screen.

  • On failure, the hook clears the current order and turns the thrown error into a user-facing string through getErrorMessage(error). That gives the consuming component a simple readable message instead of forcing it to know about ApiClientError.

  • The repeated requestId !== requestIdRef.current checks prevent a slower older request from overwriting the result of a newer one. This is the same race-condition protection pattern you used in other async hooks across the storefront.

Finally, the hook returns its public interface:

  return { order, isLoading, errorMessage };
}
  • The return shape is intentionally small and stable. That is what makes reusable hooks valuable: they hide boring async mechanics and let components think in terms of “what data do I have, am I still loading, and did something go wrong?”

  • The consuming screen does not need to know that there is a ref, an effect, or request sequencing behind the scenes. It only needs the finished interface.

This hook is a good example of a reusable abstraction doing exactly enough and no more.

Building the Checkout Success Client Component

With the API helper and hook in place, the UI layer can stay focused on rendering. The file src/components/orders/CheckoutSuccessPageClient.tsx is the actual success screen the shopper sees after checkout.

Its job is not to own request mechanics. Its job is to consume useOrder(orderId), keep the existing resilient branches for loading, error, and missing data, and then make the successful state feel polished and reassuring.

At the top of the file, the imports and hook usage set that up:

'use client';

import Link from 'next/link';
import { PageContainer } from '@/components/layout/PageContainer';
import { Button } from '@/components/ui/Button';
import { EmptyState } from '@/components/ui/EmptyState';
import { ErrorState } from '@/components/ui/ErrorState';
import { LoadingState } from '@/components/ui/LoadingState';
import { useOrder } from '@/lib/hooks/useOrder';
import { formatMoney } from '@/lib/utils/format';

export function CheckoutSuccessPageClient({ orderId }: { orderId: string }) {
  const { order, isLoading, errorMessage } = useOrder(orderId);
  • The 'use client' directive is required because this component consumes a client-side hook with React state and effects.

  • useOrder(orderId) is the only data dependency the component needs. That is a strong sign the hook is doing its job well: the component can think purely in terms of UI branches rather than fetch orchestration.

  • The component also imports shared layout and state UI primitives, which keeps the confirmation screen visually aligned with the rest of the storefront.

The non-success branches come first:

  if (isLoading) {
    return (
      <PageContainer className="py-12 md:py-16">
        <LoadingState message="Loading order confirmation..." />
      </PageContainer>
    );
  }

  if (errorMessage) {
    return (
      <PageContainer className="py-12 md:py-16">
        <ErrorState message={errorMessage} />
      </PageContainer>
    );
  }

  if (!order) {
    return (
      <PageContainer className="py-12 md:py-16">
        <EmptyState
          title="Order confirmation unavailable"
          description="The checkout completed, but the resulting order summary could not be loaded."
          action={
            <Link href="/shop">
              <Button>Continue shopping</Button>
            </Link>
          }
        />
      </PageContainer>
    );
  }
  • These branches are important because they keep the happy-path layout from carrying too many responsibilities. If loading, error, and missing-data cases were all jammed into one big success layout, the component would become harder to read and less resilient.

  • LoadingState gives the shopper clear feedback while the confirmation request is still in flight. That is especially important right after checkout, because users want reassurance that the app is still working.

  • ErrorState covers explicit request failures, such as the confirmation request failing even though checkout already completed.

  • The !order branch is a useful defensive fallback. It distinguishes “there is no error message, but we still do not have an order” from the other states, and it gives the shopper a safe next step back to the storefront.

Now look at the successful state, which is the real focus of this lesson:

  return (
    <PageContainer className="py-12 md:py-16">
      <div className="mx-auto max-w-3xl rounded-[2.5rem] border border-emerald-200 bg-white p-10 text-center shadow-sm">
        <p className="text-xs font-semibold uppercase tracking-[0.35em] text-emerald-700">Order placed</p>
        <h1 className="mt-4 font-serif text-5xl tracking-tight text-stone-950">Checkout complete</h1>
        <p className="mt-4 text-base leading-7 text-stone-600">
          Order <span className="font-mono text-sm text-stone-800">{order.id}</span> was created successfully for{' '}
          <span className="font-semibold text-stone-950">{formatMoney(order.total_cents, order.currency)}</span>.
        </p>
        <div className="mt-8 flex flex-wrap justify-center gap-3">
          <Link href="/shop">
            <Button size="lg">Continue shopping</Button>
          </Link>
        </div>
      </div>
    </PageContainer>
  );
}
  • The centered layout and contained confirmation card make the screen feel intentional and distinct from more utilitarian screens like the cart page. That matters because this is the emotional finish of the checkout flow.

  • The small eyebrow text, strong headline, and supportive descriptive copy match the polished visual language already established elsewhere in the storefront. This helps the confirmation screen feel like part of the same product instead of a generic afterthought.

  • Showing the order ID clearly is useful because it gives the shopper immediate confirmation of what was created. The monospace styling helps that identifier stand out as a concrete reference value.

  • Showing the formatted total clearly is just as important. The shopper should be able to immediately understand the financial result of the order that was created, and formatMoney() keeps that presentation aligned with the rest of the storefront.

  • The call to action stays intentionally simple: continue shopping. That is the right choice at this stage of the course because the app does not yet have a richer order-management section. A success page does not need many controls; it mainly needs to provide clarity, reassurance, and a gentle next step.

This component is straightforward in logic, but that is part of its strength. The complexity is hidden in the hook, and the UI can focus on making the checkout experience feel complete.

Wiring the Dynamic Success Route

The last file is src/app/checkout/success/[id]/page.tsx. Just like other route files in this course, it stays intentionally thin and acts as a handoff layer from the router into the client component tree.

import { CheckoutSuccessPageClient } from '@/components/orders/CheckoutSuccessPageClient';

export default async function CheckoutSuccessPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return <CheckoutSuccessPageClient orderId={id} />;
}
  • The route reads the dynamic id parameter from the URL, which is the order ID produced during checkout.

  • It then passes that value into CheckoutSuccessPageClient as orderId, keeping the route focused on routing concerns rather than data loading or UI rendering.

  • This thin-route pattern is one of the healthiest conventions in the App Router architecture you have used throughout the course. It keeps route files easy to scan and easier to maintain.

Recap

In this lesson, you completed the final data flow for the checkout confirmation experience.

You started with src/lib/api/orders.ts, where getOrder(id) provides a tiny, focused API helper for reading one order through apiRequest<Order>(). Then you built src/lib/hooks/useOrder.ts, which wraps that helper in a reusable async hook that tracks order, isLoading, and errorMessage, reacts to orderId changes with useEffect(), and preserves the same stale-request protection pattern used elsewhere in the storefront.

After that, src/components/orders/CheckoutSuccessPageClient.tsx consumed the hook and kept the loading, error, and empty-state branches intact while making the successful state feel more polished and celebratory. Finally, src/app/checkout/success/[id]/page.tsx stayed lean by simply reading the dynamic route parameter and passing it into the client component.

The main takeaway is that even a very small feature benefits from the same layered architecture as the larger ones. A tiny API helper keeps requests semantic. A reusable hook hides boring async mechanics. And the UI can focus on clarity and reassurance. That is what makes the success page feel like a real finish to the storefront flow instead of just a temporary placeholder.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal