Implementing a Go HTTP GET endpoint to fetch all products and a single product by ID using database/sql and gorilla/mux with proper error handling and parameterized queries
In this lesson we add a GET route that returns a single product by its ID as JSON. The following sections present the product model, helper functions to fetch all products and a single product from the database, the HTTP handler, and how to register the routes in your application. Code examples assume a standard Go web app using database/sql and gorilla/mux.Key topics covered:
product model and querying all products
parameterized single-row query to fetch a product by ID
Define the product struct and a helper function to return all products from the database. This function uses a simple SELECT and scans the rows into a slice of products.
Copy
import ( "database/sql")type product struct { ID int `json:"id"` Name string `json:"name"` Quantity int `json:"quantity"` Price float64 `json:"price"`}func getProducts(db *sql.DB) ([]product, error) { query := "SELECT id, name, quantity, price FROM products" rows, err := db.Query(query) if err != nil { return nil, err } defer rows.Close() products := []product{} for rows.Next() { var p product err := rows.Scan(&p.ID, &p.Name, &p.Quantity, &p.Price) if err != nil { return nil, err } products = append(products, p) } if err = rows.Err(); err != nil { return nil, err } return products, nil}
Example response returned by /products (for reference):
Register the routes for listing all products and fetching a single product by ID. This assumes your App struct includes Router *mux.Router and DB *sql.DB, and that the handler methods are defined on App.
Create a method on product that populates the struct fields based on the product ID. Always use parameterized queries to avoid SQL injection. The placeholder token depends on your SQL driver (e.g., ? for MySQL, $1 for Postgres). The example below uses ?—adjust for your driver accordingly.
Copy
import ( "database/sql")func (p *product) getProduct(db *sql.DB) error { // Use a parameterized query. Placeholder depends on the driver. query := "SELECT name, quantity, price FROM products WHERE id = ?" row := db.QueryRow(query, p.ID) err := row.Scan(&p.Name, &p.Quantity, &p.Price) if err != nil { return err } return nil}
When using database/sql, prefer QueryRow with arguments rather than formatting values into the SQL string. This avoids SQL injection and is safer.
The handler extracts the {id} path variable using mux.Vars, converts it to an integer with strconv.Atoi, constructs a product{ID: key}, calls the model method, and returns the appropriate HTTP response:
400 Bad Request when the path parameter is missing or not an integer.
404 Not Found when no matching row exists.
500 Internal Server Error for other DB errors.
200 OK with the product JSON on success.
Copy
import ( "database/sql" "errors" "net/http" "strconv" "github.com/gorilla/mux")func (app *App) getProduct(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) keyStr, ok := vars["id"] if !ok { sendError(w, http.StatusBadRequest, "invalid product ID") return } key, err := strconv.Atoi(keyStr) if err != nil { sendError(w, http.StatusBadRequest, "invalid product ID") return } p := product{ID: key} err = p.getProduct(app.DB) if err != nil { if errors.Is(err, sql.ErrNoRows) { sendError(w, http.StatusNotFound, "Product not found") } else { sendError(w, http.StatusInternalServerError, err.Error()) } return } sendResponse(w, http.StatusOK, p)}
Notes:
sendError and sendResponse are utility helpers that should write JSON responses and set the proper Content-Type and status code.
The getProduct method uses a pointer receiver so the handler can return the populated product value directly.
Example implementations for utility functions (not required, but typically like):
Copy
// sendError writes {"error": "<message>"} with the given status code.func sendError(w http.ResponseWriter, code int, message string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) fmt.Fprintf(w, `{"error":"%s"}`, message)}// sendResponse writes the value as JSON with the given status code.func sendResponse(w http.ResponseWriter, code int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) json.NewEncoder(w).Encode(v)}