CodingNic

Creating & Editing Contacts

Write the Create & Update Data Functions and Server Actions

Creating & Editing Contacts 30 min read

Write the Create & Update Data Functions and Server Actions

Write the Create & Update Data Functions and Server Actions

Add a shared input type

Open lib/types.ts and add:

typescript
export type ContactInput = {
  name: string;
  phone?: string;
  email?: string;
  company?: string;
  notes?: string;
  favorite?: boolean;
};

This is the shape of data coming from a form — everything except name is
optional, since a contact doesn’t need a phone, email, company, or notes.

Write createContact and updateContact

Open lib/contacts.ts and add:

typescript
import type { Contact, ContactInput } from "./types";

export async function createContact(input: ContactInput): Promise<Contact> {
  const result = await pool.query(
    `INSERT INTO contacts (name, phone, email, company, notes, favorite)
     VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
    [
      input.name,
      input.phone || null,
      input.email || null,
      input.company || null,
      input.notes || null,
      !!input.favorite,
    ]
  );
  return result.rows[0];
}

export async function updateContact(id: string, input: ContactInput): Promise<Contact> {
  const result = await pool.query(
    `UPDATE contacts SET
       name = $1, phone = $2, email = $3, company = $4, notes = $5,
       favorite = $6, updated_at = now()
     WHERE id = $7 RETURNING *`,
    [
      input.name,
      input.phone || null,
      input.email || null,
      input.company || null,
      input.notes || null,
      !!input.favorite,
      id,
    ]
  );
  return result.rows[0];
}

Both RETURNING * clauses hand back the row exactly as it now exists in
the database — useful in a moment, when we redirect to the new contact’s
own detail page and need its id.

Write the Server Actions

Create lib/actions.ts:

typescript
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { createContact, updateContact } from "./contacts";

function readContactInput(formData: FormData) {
  return {
    name: String(formData.get("name") || "").trim(),
    phone: String(formData.get("phone") || "").trim(),
    email: String(formData.get("email") || "").trim(),
    company: String(formData.get("company") || "").trim(),
    notes: String(formData.get("notes") || "").trim(),
    favorite: formData.get("favorite") === "on",
  };
}

export async function createContactAction(formData: FormData) {
  const input = readContactInput(formData);
  if (!input.name) {
    throw new Error("Name is required.");
  }
  const contact = await createContact(input);
  revalidatePath("/");
  redirect(`/contacts/${contact.id}`);
}

export async function updateContactAction(id: string, formData: FormData) {
  const input = readContactInput(formData);
  if (!input.name) {
    throw new Error("Name is required.");
  }
  await updateContact(id, input);
  revalidatePath("/");
  revalidatePath(`/contacts/${id}`);
  redirect(`/contacts/${id}`);
}

The "use server" directive at the top of the file is what makes every
exported function here callable directly from a <form action={...}> in a
Server Component, or from a Client Component — Next.js turns each one into
a secure endpoint automatically. There’s no route you define, no fetch
call you write, and no JSON you serialize by hand.

A checkbox’s FormData value is the string "on" when checked and
missing entirely when unchecked — that’s why favorite is read as
formData.get("favorite") === "on" rather than treated as a boolean
directly.

revalidatePath("/") tells Next.js “the data behind this path is stale,
refetch it next time it’s requested” — without it, the Home page would
keep showing its old, cached list after you add a contact. redirect(...)
sends the browser straight to the new contact’s detail page once the
Server Action finishes.

Checkpoint

There’s no UI wired up to these yet — that’s the next lesson. For now,
confirm the file compiles with no TypeScript errors and that lib/actions.ts
starts with "use server" as its very first line (it must be the first
line in the file, before any imports, or Next.js won’t treat it as a
Server Actions module).