Go REST API Project: Build a Real Backend

Go REST API Project: Build a Real Backend
You have mastered variables, logic, concurrency, and web handlers. Now, it's time to pull those separate threads together. In this comprehensive module, we will build a production-inspired REST API for a simple "Task Manager" application.
By the end of this project, you will understand how a real-world Go backend is architected, from the database connection down to the JSON response.
What You Will Build
This project implements a fully functional Task Manager REST API with four CRUD endpoints, a PostgreSQL persistence layer, and authentication middleware. The layered architecture separates data models, database logic, and HTTP handling into distinct packages — keeping the codebase testable and scalable from day one.
Clean Architecture in Go
Project Structure Overview
Let's look at how we'll organize our files. Each directory has a specific, single responsibility.
Step 1: Defining the Data (Models)
First, we define exactly what a Task looks like in our system.
Step 2: Persistence (Repository Layer)
Next, we create a layer that handles talking to the database. This keeps our database code out of our web handlers.
Step 3: Web Logic (Handlers)
Finally, our handlers bridge the gap between the HTTP request and the repository.
The REST Blueprint
List all tasksFetch every task from the database and returns them as a JSON array.
Create a taskDecodes the incoming JSON body into a Task struct and saves it permanently.
Update detailsModifies an existing task by its ID, typically used to mark a task as completed.
Remove a taskPermanently deletes a specific task record from the database.
| Task / Feature | Monolith (Single File) | Layered Architecture |
|---|---|---|
| Testing | Difficult (Logic and Networking are tangled) | Easy (Can test DB and Logic separately) |
| Scalability | Messy as files reach 1,000+ lines | Clean (Each folder stays small and focused) |
| Onboarding | Requires reading the whole file | Requires looking at the directory structure |
Step 4: Wiring It All Together in main.go
The entry point brings every layer together, registers routes, and applies middleware.
The middleware layer is covered in depth in our Go middleware patterns guide, and the web server fundamentals are explained in Building a web server with net/http.
Adding Input Validation
Never trust data arriving from the client. Decode the request body into a struct and validate before passing to the repository.
Securing Routes
Apply authentication middleware selectively to protect write operations while keeping read endpoints public:
See Go security best practices for how to implement parameterized queries to prevent SQL injection in your repository layer.
Testing the API
Go's net/http/httptest package lets you test the full handler stack without starting a server. Refer to our Go testing and unit benchmarks guide for a complete table-driven test suite pattern, including repository mocking.
Further Reading
- Go net/http package documentation — official handler and ServeMux reference.
- encoding/json documentation — Marshal, Unmarshal, and streaming JSON.
- Organizing project packages is covered in Go modules and packages.
Common Mistakes and How to Avoid Them
Building a Go REST API for the first time comes with predictable pitfalls. Here are the five most common mistakes and how to fix them.
1. Not closing the request body. Every r.Body must be closed after reading — the Go HTTP client does not do this automatically. A missed defer r.Body.Close() quietly exhausts file descriptors under load. Always add it immediately after you check for read errors.
2. Writing to the response after calling http.Error. Once you call http.Error(w, ...), the response status and headers are committed. Any subsequent call to w.Write or w.WriteHeader has no effect — but it also does not panic, so the bug is silent. Always return immediately after an error response.
3. Ignoring the Content-Type header. Browsers and API clients use Content-Type to decide how to parse a response. A Go handler that sends JSON without first calling w.Header().Set("Content-Type", "application/json") may cause client-side parse failures or security rejections in strict CORS environments.
4. Putting all routes in main.go. As your API grows beyond five or six endpoints, a single main.go becomes unmaintainable. Move handler registration into a dedicated routes.go file and keep main.go focused on wiring up dependencies. This matches the layered architecture described in Effective Go and the standard library net/http documentation.
5. Missing database connection pool configuration. sql.Open does not open a connection — it only validates the data source name. Call db.Ping() at startup to confirm connectivity. Then set db.SetMaxOpenConns, db.SetMaxIdleConns, and db.SetConnMaxLifetime to prevent connection exhaustion under real traffic.
FAQ
Q: Should I use a third-party router (Gin, Chi) or the standard net/http?
For most APIs, the Go 1.22+ ServeMux with method-based routing (GET /api/tasks/{id}) is sufficient and carries zero external dependencies. Reach for Chi or Gin when you need advanced features like sub-router grouping, built-in middleware bundles, or OpenAPI integration. The Go net/http package documentation documents all pattern-matching rules introduced in 1.22.
Q: How do I handle pagination in a REST API?
Accept limit and offset (or cursor) query parameters, parse them with r.URL.Query().Get("limit"), convert with strconv.Atoi, apply bounds checking (max 100 per page), and pass them as parameters to your repository query. Return a meta object in the JSON response containing the total count so clients can render page controls.
Q: What is the safest way to return errors to API clients?
Distinguish between operational errors (invalid input, not found) and unexpected errors (database failure). For client errors, return a structured JSON body: {"error": "title is required"} with the appropriate 4xx status. For server errors, return a generic {"error": "internal server error"} with 500 — never expose stack traces or database error messages to clients. Log the full detail server-side using the Go standard log or slog packages.
Next Steps
Congratulations! You have just architected a real-world web application. But as your user base grows, your database calls might become a bottleneck. In our next tutorial, we will explore Caching in Go with Redis, learning how to speed up your API by storing frequently accessed data in memory.
Common Mistakes When Building a Go REST API
1. Ignoring error returns from the router and server
http.ListenAndServe returns an error that is frequently ignored. Always wrap it: log.Fatal(http.ListenAndServe(":8080", router)) so a port conflict or binding failure surfaces immediately.
2. Writing business logic inside handlers Handlers that query the database, format responses, and apply business rules become untestable. Move logic into a service layer and pass it to the handler via dependency injection — your tests will thank you.
3. Not setting response Content-Type
Omitting w.Header().Set("Content-Type", "application/json") before writing a JSON body causes some clients to misparse the response. Always set headers before calling w.WriteHeader() or json.NewEncoder(w).Encode().
4. Leaking database connections
Forgetting to call rows.Close() or stmt.Close() after a database operation leaks connections from the pool. Use defer rows.Close() immediately after a successful db.Query() call.
5. Hard-coding configuration
Port numbers, DSN strings, and secret keys embedded in source code are a security risk. Use environment variables via os.Getenv() or a configuration package like godotenv.
Frequently Asked Questions
What router should I use for a production Go REST API?
The standard library net/http ServeMux works for simple APIs, but for path parameters and method-based routing, gorilla/mux or chi are the most widely adopted choices. Chi is particularly popular for its lightweight middleware chain.
How do I handle authentication in a Go REST API?
The most common approach is JWT-based authentication via middleware. The middleware extracts the token from the Authorization header, validates it with a library like golang-jwt/jwt, and either passes the request to the next handler or returns a 401. Session-based auth is also possible using a cookie store.
Should I use GORM or raw database/sql?
For rapid development and simple CRUD, GORM reduces boilerplate significantly. For performance-critical paths or complex queries, raw database/sql with the pgx driver gives you full control. Many production services start with GORM and drop to raw SQL only for the queries where performance matters. See the official database/sql documentation for the standard interface.
Continue Learning
Building a REST API involves a lot of JSON serialisation. For a deep dive into how Go handles encoding, struct tags, custom marshallers, and edge cases, see the Go JSON Marshalling and Unmarshalling guide.
