Connecting to a Database
The Node.js ORM landscape vs Go
Section titled “The Node.js ORM landscape vs Go”TypeScript developers are used to choosing between Prisma, TypeORM, Drizzle, Knex, or Sequelize. Go has similar options — GORM, sqlc, Bun, Ent — but the stdlib database/sql package is a first-class option that is well worth knowing.
| Concern | Node.js options | Go options |
|---|---|---|
| Low-level | pg, mysql2 (raw drivers) | database/sql + driver (e.g. lib/pq, pgx) |
| Query builder | knex | squirrel, raw database/sql |
| Full ORM | TypeORM, Drizzle, Sequelize | GORM, ent, bun |
| Codegen from SQL | — | sqlc (SQL → type-safe Go) |
This lesson uses database/sql with the lib/pq driver for PostgreSQL — the most transferable baseline.
Opening a connection pool
Section titled “Opening a connection pool”// Node.js — pg connection poolimport { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL,});
export default pool;// Go — database/sql + lib/pqimport ( "database/sql" "log" "os"
_ "github.com/lib/pq" // register driver via side-effect import)
func NewDB() *sql.DB { dsn := os.Getenv("DATABASE_URL") db, err := sql.Open("postgres", dsn) if err != nil { log.Fatalf("sql.Open: %v", err) } if err := db.Ping(); err != nil { log.Fatalf("db.Ping: %v", err) } db.SetMaxOpenConns(25) db.SetMaxIdleConns(5) return db}A store layer — the repository pattern
Section titled “A store layer — the repository pattern”Rather than calling db directly from handlers, wrap database logic in a store struct. This mirrors the Repository pattern in NestJS and keeps handlers thin.
// NestJS — repository injection@Injectable()export class BooksRepository { constructor( @InjectRepository(Book) private readonly repo: Repository<Book>, ) {}
async findById(id: string): Promise<Book | null> { return this.repo.findOneBy({ id }); }}// Go — store struct (manual DI)type BookStore struct { db *sql.DB}
func NewBookStore(db *sql.DB) *BookStore { return &BookStore{db: db}}
func (s *BookStore) FindByID(ctx context.Context, id string) (Book, error) { const q = `SELECT id, title, author, year FROM books WHERE id = $1` var b Book err := s.db.QueryRowContext(ctx, q, id).Scan( &b.ID, &b.Title, &b.Author, &b.Year, ) if errors.Is(err, sql.ErrNoRows) { return Book{}, fmt.Errorf("FindByID: %w", ErrNotFound) } if err != nil { return Book{}, fmt.Errorf("FindByID: %w", err) } return b, nil}Inserting a row
Section titled “Inserting a row”Always use parameterized queries ($1, $2, …). Never interpolate values into SQL strings — that is a SQL injection vulnerability.
// Prisma insertconst book = await prisma.book.create({ data: { title, author, year },});// database/sql insert — parameterized queryfunc (s *BookStore) Insert(ctx context.Context, req CreateBookRequest) (Book, error) { const q = ` INSERT INTO books (title, author, year) VALUES ($1, $2, $3) RETURNING id, title, author, year ` var b Book err := s.db.QueryRowContext(ctx, q, req.Title, req.Author, req.Year, ).Scan(&b.ID, &b.Title, &b.Author, &b.Year) if err != nil { return Book{}, fmt.Errorf("Insert: %w", err) } return b, nil}Wiring the store into a handler via main.go
Section titled “Wiring the store into a handler via main.go”db := NewDB()
bookStore := store.NewBookStore(db)bookHandler := handler.NewBookHandler(bookStore)
api := e.Group("/api")api.GET("/books/:id", bookHandler.Get)api.POST("/books", bookHandler.Create)No DI container is needed — Go’s type system and constructor functions handle it explicitly.
Brief mentions of higher-level options
Section titled “Brief mentions of higher-level options”- sqlc — write plain SQL files, run
sqlc generate, get type-safe Go functions. Zero reflection overhead. Best for teams that want SQL control. - GORM — ActiveRecord-style ORM; familiar if you come from Sequelize. Costs some query transparency.
- pgx/pgxpool — a high-performance PostgreSQL driver (over
database/sql). Recommended for production (see Go memory file). SameQueryRowContext/Scanpattern, better throughput.
Run this locally — it needs a live PostgreSQL database, the
lib/pqdriver module, and a network port.