CodingNic

Contact Detail Page

Build the Contact Detail Page

Contact Detail Page 25 min read

Build the Contact Detail Page

Build the Contact Detail Page

Add a lookup-by-id function

Open lib/contacts.ts and add:

typescript
export async function getContactById(id: string): Promise<Contact | null> {
  const result = await pool.query(`SELECT * FROM contacts WHERE id = $1`, [id]);
  return result.rows[0] || null;
}

This returns null rather than throwing when nothing matches — that’s the
signal the page will use to show a proper 404 instead of crashing.

Build the page

Open app/contacts/[id]/page.tsx and replace the placeholder:

tsx
import { notFound } from "next/navigation";
import Link from "next/link";
import { getContactById } from "@/lib/contacts";
import { initials } from "@/lib/utils";

export const dynamic = "force-dynamic";

export default async function ContactDetailPage({ params }: { params: { id: string } }) {
  const contact = await getContactById(params.id);
  if (!contact) notFound();

  return (
    <div className="detail-card">
      <div className="detail-header">
        <div className="avatar">{initials(contact.name)}</div>
        <div>
          <h1>{contact.name}</h1>
          {contact.company ? <div className="company">{contact.company}</div> : null}
        </div>
      </div>

      {contact.phone ? (
        <div className="detail-row">
          <div className="detail-label">Phone</div>
          <div className="detail-value"><a href={`tel:${contact.phone}`}>{contact.phone}</a></div>
        </div>
      ) : null}
      {contact.email ? (
        <div className="detail-row">
          <div className="detail-label">Email</div>
          <div className="detail-value"><a href={`mailto:${contact.email}`}>{contact.email}</a></div>
        </div>
      ) : null}
      {contact.notes ? (
        <div className="detail-row">
          <div className="detail-label">Notes</div>
          <div className="detail-value notes-value">{contact.notes}</div>
        </div>
      ) : null}
      {!contact.phone && !contact.email && !contact.notes ? (
        <div className="detail-row">
          <div className="detail-value notes-value">No further details.</div>
        </div>
      ) : null}

      <div className="detail-actions">
        <Link href="/" className="btn btn-ghost">Back to all contacts</Link>
      </div>
    </div>
  );
}

A few things to notice:

  • params.id comes from the [id] folder name — Next.js matches
    /contacts/abc123 and hands you { id: "abc123" } automatically.
  • notFound() is a Next.js function that immediately renders the
    framework’s built-in 404 page and stops executing the rest of the
    component. It’s the standard way to handle “this resource doesn’t exist”
    in a Server Component.
  • Each detail row (phone, email, notes) only renders if that field
    has a value — no empty “Phone: —” rows cluttering the page.

Edit and Delete buttons belong here too, but they need Server Actions that
don’t exist yet — you’ll add them in Modules 5 and 6.

Test it

Click any contact card from the Home page. You should land on its detail
page with phone/email as clickable tel:/mailto: links. Now try visiting
a URL with a made-up id, like /contacts/00000000-0000-0000-0000-000000000000
— you should see Next.js’s 404 page, not a crash.

Checkpoint

Every contact card links to a working detail page showing that contact’s
full information, and an invalid id correctly renders a 404 instead of an
error.