Build the Contact Detail Page
Build the Contact Detail Page
Add a lookup-by-id function
Open lib/contacts.ts and add:
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:
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.idcomes from the[id]folder name — Next.js matches
/contacts/abc123and 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.