store.go — Database Layer

Opens a connection pool, auto-creates the table, and provides CRUD methods. All methods accept context.Context for cancellation.

package main

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"time"

	_ "github.com/lib/pq"
)

var ErrNotFound = errors.New("bookmark not found")

type BookmarkStore struct {
	db *sql.DB
}

func OpenDB(dsn string) (*sql.DB, error) {
	db, err := sql.Open("postgres", dsn)
	if err != nil {
		return nil, fmt.Errorf("open db: %w", err)
	}
	db.SetMaxOpenConns(25)
	db.SetMaxIdleConns(5)
	db.SetConnMaxLifetime(5 * time.Minute)
	if err := db.Ping(); err != nil {
		return nil, fmt.Errorf("ping db: %w", err)
	}
	return db, nil
}

func NewBookmarkStore(db *sql.DB) *BookmarkStore {
	return &BookmarkStore{db: db}
}

func (s *BookmarkStore) Init(ctx context.Context) error {
	_, err := s.db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS bookmarks (
			id         SERIAL PRIMARY KEY,
			url        TEXT NOT NULL,
			title      TEXT NOT NULL,
			created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
		)`)
	return err
}

func (s *BookmarkStore) Create(ctx context.Context, url, title string) (Bookmark, error) {
	var b Bookmark
	err := s.db.QueryRowContext(ctx,
		"INSERT INTO bookmarks (url, title) VALUES ($1, $2) RETURNING id, url, title, created_at",
		url, title,
	).Scan(&b.ID, &b.URL, &b.Title, &b.CreatedAt)
	if err != nil {
		return Bookmark{}, fmt.Errorf("create bookmark: %w", err)
	}
	return b, nil
}

func (s *BookmarkStore) GetByID(ctx context.Context, id int) (Bookmark, error) {
	var b Bookmark
	err := s.db.QueryRowContext(ctx,
		"SELECT id, url, title, created_at FROM bookmarks WHERE id = $1", id,
	).Scan(&b.ID, &b.URL, &b.Title, &b.CreatedAt)
	if errors.Is(err, sql.ErrNoRows) {
		return Bookmark{}, ErrNotFound
	}
	if err != nil {
		return Bookmark{}, fmt.Errorf("get bookmark: %w", err)
	}
	return b, nil
}

func (s *BookmarkStore) List(ctx context.Context) ([]Bookmark, error) {
	rows, err := s.db.QueryContext(ctx,
		"SELECT id, url, title, created_at FROM bookmarks ORDER BY created_at DESC",
	)
	if err != nil {
		return nil, fmt.Errorf("list bookmarks: %w", err)
	}
	defer rows.Close()

	var bookmarks []Bookmark
	for rows.Next() {
		var b Bookmark
		if err := rows.Scan(&b.ID, &b.URL, &b.Title, &b.CreatedAt); err != nil {
			return nil, fmt.Errorf("scan bookmark: %w", err)
		}
		bookmarks = append(bookmarks, b)
	}
	return bookmarks, rows.Err()
}

func (s *BookmarkStore) Delete(ctx context.Context, id int) error {
	result, err := s.db.ExecContext(ctx, "DELETE FROM bookmarks WHERE id = $1", id)
	if err != nil {
		return fmt.Errorf("delete bookmark: %w", err)
	}
	rows, _ := result.RowsAffected()
	if rows == 0 {
		return ErrNotFound
	}
	return nil
}

💻 Run locally

Copy the code above and run it on your machine

© 2026 ByteLearn.dev. Free courses for developers. · Privacy