CodingNic

Data Layer & Home List

Build the Database Pool, Types, and Contacts Query

Data Layer & Home List 25 min read

Build the Database Pool, Types, and Contacts Query

Build the Database Pool, Types, and Contacts Query

Create the connection pool

Create lib/db.ts. This is the one place in the app that knows how to
connect to Postgres — every query function will import pool from here.

typescript
import { Pool } from "pg";

declare global {
  // eslint-disable-next-line no-var
  var _pgPool: Pool | undefined;
}

function createPool() {
  const connectionString = process.env.DATABASE_URL;
  if (!connectionString) {
    throw new Error(
      "DATABASE_URL is not set. Copy .env.example to .env.local and set your Postgres connection string."
    );
  }
  return new Pool({
    connectionString,
    ssl: connectionString.includes("sslmode=require")
      ? { rejectUnauthorized: false }
      : undefined,
  });
}

export const pool = global._pgPool || createPool();
if (process.env.NODE_ENV !== "production") global._pgPool = pool;

The global._pgPool trick matters in development: Next.js hot-reloads your
server code on every save, and without this, each reload would open a new
pool of database connections without closing the old one. Reusing a global
keeps you on one pool across reloads.

The ssl line is why we mentioned ?sslmode=require in the last lesson —
if your connection string includes it, the pool automatically enables SSL.

Define the Contact type

Create lib/types.ts:

typescript
export type Contact = {
  id: string;
  name: string;
  phone: string | null;
  email: string | null;
  company: string | null;
  notes: string | null;
  favorite: boolean;
  created_at: string;
  updated_at: string;
};

This matches the contacts table column-for-column. Nullable columns
(phone, email, company, notes) are typed as string | null, since
that’s exactly what Postgres will hand back for an empty field.

Write the query function

Create lib/contacts.ts:

typescript
import { pool } from "./db";
import type { Contact } from "./types";

export async function getContacts(): Promise<Contact[]> {
  const result = await pool.query(
    `SELECT * FROM contacts ORDER BY lower(name) ASC`
  );
  return result.rows;
}

lower(name) sorts case-insensitively, so “ada” and “Ada” land next to each
other instead of being split by capitalization.

This is deliberately the simplest version of this function — no search, no
pagination yet. We’ll extend it in Module 3, once there’s a UI that needs
those features.

Add a couple of test contacts

There’s no seed script yet, so add a few rows directly:

bash
psql "$DATABASE_URL" -c "
INSERT INTO contacts (name, phone, email, company, favorite) VALUES
  ('Ada Lovelace', '555-0101', 'ada@example.com', 'Analytical Engines Ltd', true),
  ('Grace Hopper', '555-0102', 'grace@example.com', 'Navy Research', true),
  ('Alan Turing', '555-0103', 'alan@example.com', NULL, false);
"

Checkpoint

You can’t see anything in the browser yet — getContacts isn’t called from
any page. But if you want to sanity-check it, temporarily add
console.log(await getContacts()) to the top of app/page.tsx’s component
and check your terminal: you should see three contact objects logged when
you load the page. Remove the console.log before moving on.