GoProjects

Go REST API Project: Build a Real Backend

TT
TopicTrick Team
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.

    text

    Step 1: Defining the Data (Models)

    First, we define exactly what a Task looks like in our system.

    go

    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.

    go

    Step 3: Web Logic (Handlers)

    Finally, our handlers bridge the gap between the HTTP request and the repository.

    go

    The REST Blueprint

    GET /tasksList all tasks

    Fetch every task from the database and returns them as a JSON array.

    POST /tasksCreate a task

    Decodes the incoming JSON body into a Task struct and saves it permanently.

    PUT /tasks/:idUpdate details

    Modifies an existing task by its ID, typically used to mark a task as completed.

    DELETE /tasks/:idRemove a task

    Permanently deletes a specific task record from the database.

    Task / FeatureMonolith (Single File)Layered Architecture
    TestingDifficult (Logic and Networking are tangled)Easy (Can test DB and Logic separately)
    ScalabilityMessy as files reach 1,000+ linesClean (Each folder stays small and focused)
    OnboardingRequires reading the whole fileRequires 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.

    go

    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.

    go

    Securing Routes

    Apply authentication middleware selectively to protect write operations while keeping read endpoints public:

    go

    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.

    go

    Further Reading

    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.