
August 6, 2026
Building a “Hello World” API is still the fastest way to understand a backend stack, but in 2026 it means more than returning a plain text string. In modern Go backend development, a Hello World API is usually a tiny HTTP service with JSON responses, health checks, configuration, structured error handling, and a real database connection. It is the smallest useful slice of a production service: simple enough to learn quickly, but realistic enough to reveal how the moving parts fit together.
In this guide, you’ll build a lightweight Go API backed by PostgreSQL, using current Go tooling and practices. Go 1.24 is the latest release in the official release history, and the language continues to emphasize compatibility while adding practical improvements such as generic type aliases and tool directives in modules. Meanwhile, PostgreSQL remains a strong default for durable, relational backend systems, with official documentation emphasizing reliability and write-ahead logging as core strengths. (go.dev)
The goal here is not to build a giant framework-dependent application. Instead, you’ll learn how to create a clean starter project that can scale: a small API server, safe database access via Go’s standard database/sql package, a sample table, a real insert/read route, and the testing and operational foundations you need to move toward production. Go’s standard database/sql package is designed as a generic interface around SQL databases and is intended to be used with a driver, which makes it a solid base for PostgreSQL-backed services. (pkg.go.dev)

A Hello World API used to mean “start a server and return a string.” That is still a valid first step, but modern backend development expects a bit more discipline from the start. A useful starter API should prove that your project can accept requests, validate configuration, talk to a database, return JSON, and surface errors in a predictable way. In other words, it should test the whole path from HTTP entrypoint to persistence and back.
For Go developers, this matters because Go is often chosen for services that need to be small, fast, and easy to deploy. The standard library provides a strong baseline for HTTP and SQL, and the broader ecosystem makes it straightforward to add a PostgreSQL driver and keep the code path simple. The official database/sql package is intentionally generic, which means you can write clean application code while leaving driver-specific behavior to a well-supported library. (pkg.go.dev)
A modern Hello World API should also reflect how real services are operated. That means a /health endpoint for monitoring, environment-based configuration for portability, and separate code paths for application logic and database access. If you skip these concerns in the first version, you usually end up refactoring them later under pressure. Starting with them early gives you a stable foundation.
Think of this project as a miniature template for future services. The example records and endpoints will be simple, but the structure will be realistic. You’ll use that structure to practice the patterns that matter in real systems: resource management, request context propagation, safe database connections, and meaningful test coverage. This is the point where “Hello World” stops being toy code and starts becoming a starter service.
Go remains a strong choice for APIs in 2026 because it continues to balance simplicity, performance, and operational friendliness. The language has stayed conservative in the best way: the Go project emphasizes backward compatibility, so teams can upgrade without constant rewrites. The release history shows Go 1.24 as the latest major release, following Go 1.23, with the same compatibility promise maintained across versions. (go.dev)
Recent language updates also keep Go relevant without making it complicated. Go 1.23 added language support for ranging over iterator functions and introduced preview support for generic type aliases. Go 1.24 fully supports generic type aliases and adds tool directives in go.mod, which make it easier to track executable dependencies in module files without the older tools.go workaround. These are small but important quality-of-life improvements for backend developers working in larger codebases. (go.dev)
Go is also a good fit for API services because its standard library is unusually capable. The HTTP server, JSON encoding, context handling, and SQL database APIs are all built in or officially supported. That means you can build a production-grade service without pulling in a large framework just to return a response. This is especially attractive for teams that value explicit code, low runtime overhead, and easy deployment. The database/sql package documentation describes it as a generic interface around SQL databases and notes that it must be used with a driver, which is exactly the kind of clean abstraction a backend service needs. (pkg.go.dev)
From an architectural standpoint, Go also plays well with containers and cloud deployment because compiled binaries are easy to ship and run. For APIs, that means fewer moving parts at startup and a smaller surface area for operational issues. If your goal is to create a service that is understandable to new team members but still robust enough for production, Go remains a very strong default in 2026.
PostgreSQL remains one of the best choices for application backends because it combines reliability, strong transactional behavior, and deep SQL features. The official documentation emphasizes reliability and the write-ahead log as central to its design, which is one reason it remains a default choice for systems that cannot afford data loss or inconsistent state. (postgresql.org)
For API backends, PostgreSQL’s biggest advantage is that it is boring in the right way. It handles relational data well, enforces constraints, and gives you tools to model real business data instead of forcing you into workarounds. It also supports modern data patterns, including jsonb, which is useful when an API needs a mix of structured relational columns and flexible metadata. The PostgreSQL documentation includes dedicated coverage of jsonb containment and existence behavior, which reflects how mature these features are. (postgresql.org)
Operationally, PostgreSQL is also easy to reason about. It has a rich ecosystem, mature tooling, and wide support across hosting providers, container images, and managed cloud services. That matters because the database is usually not the place where you want surprises. If your API stores user accounts, orders, audit events, or configuration, PostgreSQL gives you a durable foundation that scales from local development to production.
For a starter service, PostgreSQL is especially useful because it encourages good habits. You learn how to create schemas, define keys, use migrations, and handle errors at the database boundary. Those habits transfer directly to larger systems. Even if your production stack later includes caching, queues, or search systems, PostgreSQL often remains the primary source of truth.
The best way to start is with a small, explicit layout. You do not need a full framework to build a useful API. A lightweight structure is easier to understand, easier to test, and easier to replace later if needed. Since Go’s standard tools are already strong, a small module with a few packages is usually enough for a starter project. Go modules are the official dependency management solution, and the database/sql package is built to work cleanly with drivers and connection pools. (pkg.go.dev)
A practical setup looks like this:
hello-api/
├── cmd/api/main.go
├── internal/config/
├── internal/db/
├── internal/handlers/
├── internal/models/
├── internal/repository/
├── migrations/
├── tests/
├── go.mod
└── .env.exampleThis layout keeps the entrypoint in cmd/api, while internal packages hold configuration, database code, handlers, and repositories. That separation matters because it prevents your HTTP layer from becoming tightly coupled to your data layer. It also makes tests easier to write.
For local development, install:
Go 1.24 or later for the latest language and tooling improvements. (go.dev)
PostgreSQL 16 or newer if you want to stay close to current stable documentation and behavior. The current documentation set is published as PostgreSQL 18.4, with prior versions also documented. (postgresql.org)
A PostgreSQL driver for Go, such as pgx via its database/sql compatibility layer or another driver that implements the standard interfaces. The core point is that database/sql needs a driver, not that your application must depend on a specific framework. (pkg.go.dev)

Create your module:
mkdir hello-api && cd hello-api
go mod init example.com/hello-apiThen install a PostgreSQL driver and a small router if you want one. You can also build with only the standard library. For a starter guide, a lightweight router is fine, but avoid excessive abstraction. The point of this project is clarity.
A good starter schema should be small but realistic. For this guide, create a simple messages table that stores sample API content. Keep the columns minimal so you can focus on the connection, insert, and read paths.
Example SQL:
CREATE TABLE IF NOT EXISTS messages (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);This schema gives you an auto-incrementing primary key, two required text fields, and a timestamp. It is simple enough to use in a tutorial but realistic enough to represent a basic content entity.
To connect safely from Go, use a connection pool and keep credentials out of source code. The database/sql package is designed around a *sql.DB pool, and its examples and documentation show standard patterns for initialization, context-aware operations, and prepared statement reuse. (pkg.go.dev)
Use environment variables such as:
APP_PORT=8080
DATABASE_URL=postgres://postgres:password@localhost:5432/hello_api?sslmode=disableThen in Go, read configuration once at startup. A basic connection setup might look like this:
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("open database: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
log.Fatalf("ping database: %v", err)
}The important part is not just “open the database” but “verify it is reachable before serving traffic.” That keeps startup behavior predictable and makes failures obvious. In production, you should also tune pool settings such as maximum open connections, maximum idle connections, and connection lifetime according to your deployment model. Go’s database pool is intended for concurrent use and is a central part of the standard SQL workflow. (pkg.go.dev)
From a safety perspective, never interpolate raw user input into SQL strings. Use parameterized queries through the database driver interface. This protects against injection and keeps your code easier to maintain.
Your first endpoint should prove that the service is alive and responding correctly. Instead of plain text, return JSON so the service matches real API conventions.
A simple /hello endpoint can return a structured payload:
{
"message": "Hello, World!"
}In Go:
type HelloResponse struct {
Message string `json:"message"`
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(HelloResponse{Message: "Hello, World!"})
}Add a /health endpoint that checks whether the API is running and whether the database is reachable. A health endpoint is one of the simplest but most useful operational features you can add early.
func healthHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
http.Error(w, `{"status":"unhealthy"}`, http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
}
}This is a better starter pattern than merely checking if the process is running. A service can be “up” while its database is down, so a database-aware health route gives you more realistic operational information. The use of PingContext fits well with the context-aware style encouraged by the standard SQL package. (pkg.go.dev)
At this stage, keep routing simple. Whether you use net/http directly or a very lightweight router, the goal is the same: keep the surface area small and understandable. The API should start cleanly, return JSON, and fail gracefully if the database is unavailable.
Once the basic API works, add a route that exercises the whole stack: HTTP request, repository logic, SQL insert, SQL read, and JSON response. This is where the project stops being a shell and becomes a real backend service.
A common pattern is:
POST /messages to create a record
GET /messages/{id} to read it back
Define a model:
type Message struct {
ID int64 `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
CreatedAt time.Time `json:"created_at"`
}For inserts, use parameterized SQL:
const insertMessage = `
INSERT INTO messages (title, body)
VALUES ($1, $2)
RETURNING id, created_at
`Then:
err := db.QueryRowContext(ctx, insertMessage, msg.Title, msg.Body).
Scan(&msg.ID, &msg.CreatedAt)For reads:
const selectMessage = `
SELECT id, title, body, created_at
FROM messages
WHERE id = $1
`
err := db.QueryRowContext(ctx, selectMessage, id).
Scan(&msg.ID, &msg.Title, &msg.Body, &msg.CreatedAt)The database layer should translate SQL errors into application-level errors where appropriate. For example, a missing row should become a 404 Not Found, while a constraint violation might become a 400 Bad Request depending on your API contract.
A small repository abstraction is enough:
type MessageRepository interface {
Create(ctx context.Context, msg *Message) error
GetByID(ctx context.Context, id int64) (*Message, error)
}This keeps your handlers free from SQL details and makes testing easier. The SQL package documentation and examples support this kind of clear boundary, and the underlying driver abstraction is meant to support exactly this style of application code. (pkg.go.dev)

Production readiness starts with predictable error handling. Your API should never expose raw stack traces or driver internals to clients. Instead, convert internal errors into clean HTTP responses and log the details server-side.
A practical approach is:
400 Bad Request for invalid input
404 Not Found for missing records
500 Internal Server Error for unexpected failures
503 Service Unavailable for database connectivity issues
Validation should happen as early as possible in the request path. If a title is required, reject the request before calling the database. This keeps your error messages clear and reduces unnecessary load.
Configuration should come from environment variables, not hard-coded values. At minimum, define:
APP_PORT
DATABASE_URL
APP_ENV
LOG_LEVEL
A small config package can load and validate these values at startup. If required configuration is missing, fail fast rather than starting a broken service.
type Config struct {
Port string
DatabaseURL string
Environment string
}This also helps in containerized deployments where environment variables are the standard configuration mechanism. Go’s tooling and standard library make this pattern straightforward, and database/sql supports context-aware operations that pair well with deadlines and cancellation from incoming requests. (pkg.go.dev)
Error logging should be structured and consistent. Even if you start with the standard library logger, format messages so they can later be ingested by centralized logging systems. Include request IDs if available, and propagate r.Context() into repository calls so that request cancellations stop work cleanly.
Finally, keep secrets out of version control. Use .env.example for documentation, but never commit actual credentials. This is a basic habit, but it becomes essential as soon as more than one environment exists.
Testing a Go API backed by PostgreSQL should happen at multiple levels. The smallest useful set is:
unit tests for handlers and validation
repository tests against a real or disposable database
integration tests for full request flows
Unit tests should focus on behavior, not SQL. For example, test that invalid JSON returns a 400 and that a valid hello endpoint returns the expected JSON payload.
Repository tests are where PostgreSQL matters most. Because database/sql is designed to work with a driver and context-aware operations, it fits naturally into integration tests that run against a real database instance. (pkg.go.dev)
A solid database test strategy includes:
using a dedicated test database
resetting state between tests
wrapping test cases in transactions when possible
avoiding shared mutable data
verifying migrations before running repository tests
For example, you might:
Start PostgreSQL locally or in CI.
Apply migrations to a test database.
Run tests that insert and query sample rows.
Clean up by truncating tables or rolling back transactions.
If you use table-driven tests in Go, you can cover many cases with very little code. That is one reason Go remains productive for backend services: the language encourages straightforward test design without heavy ceremony.
Integration tests should hit real HTTP handlers with httptest. That lets you verify routing, content types, status codes, and response shapes. A good integration test for this project would create a message via POST /messages, read it back via GET /messages/{id}, and confirm that the returned JSON matches what was inserted.
Testing does not have to be exhaustive on day one. It does need to protect the core flow. If the route can create and retrieve a record in a test database, you have proven the essential contract of the service.
Once the starter API works, the next step is making it reproducible. Docker is the most practical first move because it removes “works on my machine” problems from local setup and CI. A small multi-stage Dockerfile can build the Go binary and run it in a minimal runtime image.
After that, add migrations. A migration tool gives you versioned schema changes and a repeatable history of database evolution. This is critical because real systems do not stay on the initial schema. As your API grows, migrations help you add columns, create indexes, backfill data, and manage rollbacks in a controlled way.
Observability should come next. At minimum, add:
structured logs
request timing
database query timings
health and readiness endpoints
optional metrics for request rates and errors
These are not luxury features. They are the difference between a toy service and something you can safely operate.
Deployment depends on your environment, but the usual path is straightforward:
build a container image
provision PostgreSQL, preferably managed if available
inject environment variables or secrets
run the service behind a load balancer or ingress
monitor health endpoints and logs
Go’s release cadence and compatibility promise make this easier than it might be in more volatile ecosystems. PostgreSQL’s maturity and reliability features make it a strong persistence layer for the long term. Together, they form a stack that is simple enough for a starter project and serious enough for production.
A Hello World API in Go with PostgreSQL is no longer just a first exercise; it is a compact blueprint for how modern backend services are built. Go gives you a stable, fast, and easy-to-deploy runtime with strong standard library support, while PostgreSQL gives you reliable transactional storage and mature relational semantics. (go.dev)
The key takeaway is to start small but not naive. Use JSON, use environment-based configuration, connect to PostgreSQL safely, write one real database-backed route, and test the whole flow. If you do that, your “Hello World” API becomes a practical starter template instead of disposable demo code.
From here, the natural next steps are migrations, Docker, observability, and deployment. Those additions turn the starter into a service you can actually maintain. The foundation you build now will pay off every time you create a new API in the future.