I’ve been building web services in Go for a while now, and I keep coming back to a simple question: why do we accept so much uncertainty between our database and our HTTP handlers? If you’ve ever chased a runtime error caused by a mismatched column name or an incorrect data type, you know the frustration. That’s exactly why I started combining sqlc with the Chi router. This pairing brings a level of clarity and safety to Go web development that feels both practical and powerful. Let’s walk through how it works, and I encourage you to follow along—this approach might just change how you structure your next project.
Think about the last time you changed a database schema. How many parts of your code did you have to check? With sqlc, you write plain SQL. The tool reads your queries and your database schema, then generates type-safe Go code for you. There’s no runtime ORM magic. The types in your Go program match your database columns exactly. If a query doesn’t align with the schema, your code won’t compile. This catches mistakes early, long before they reach production.
Here’s a glimpse of what sqlc does. You start with a SQL query in a .sql file.
-- query.sql
-- name: GetUser :one
SELECT id, name, email FROM users
WHERE id = $1;
From this, sqlc generates a Go method with a concrete struct.
// db.go (generated by sqlc)
type User struct {
ID int32
Name string
Email string
}
func (q *Queries) GetUser(ctx context.Context, id int32) (User, error) {
// ... generated implementation
}
Now, your data layer is ready. But how do you connect it to the web? This is where Chi shines. Chi is a lightweight router built on Go’s standard net/http package. It doesn’t impose a framework. Instead, it gives you a clean way to organize routes and middleware. When you pair it with sqlc’s generated code, your HTTP handlers become straightforward and safe.
Consider a simple handler to fetch a user. Notice how the types flow from the database query directly into the HTTP response.
package main
import (
"encoding/json"
"net/http"
"strconv"
)
type Handler struct {
queries *db.Queries // Generated by sqlc
}
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
// Chi's URLParam is used to get the route parameter
userIDStr := chi.URLParam(r, "id")
userID, err := strconv.ParseInt(userIDStr, 10, 32)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
// Use the type-safe sqlc method
user, err := h.queries.GetUser(r.Context(), int32(userID))
if err != nil {
// Handle not found or other DB errors
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
Can you see the benefit? The GetUser method returns a concrete User struct. There’s no interface{} or map[string]interface{} in sight. Your handler knows the exact shape of the data. This makes the code easier to read, test, and refactor.
What about more complex operations, like creating a user with data from an HTTP request? The pattern remains clear. You define a SQL query for insertion, and sqlc generates a method with the correct parameters. Your handler parses the JSON request, validates it, and passes the strongly-typed data to the database layer. Any type mismatch is a compile-time error, not a runtime surprise.
This approach is perfect for API services. Chi’s middleware handles cross-cutting concerns—authentication, logging, request ID propagation—while sqlc ensures your data logic is solid. You get the performance of handwritten SQL and the safety of generated Go types. It’s a combination that respects your time and reduces bugs.
So, what’s stopping you from trying this in your current project? The setup is minimal. You define your SQL, run sqlc, and wire the generated queries into your Chi routes. The immediate feedback from the compiler is incredibly rewarding. You spend less time debugging and more time building features.
I’ve found that this integration scales well. As your service grows, the clear separation between the database layer and the HTTP layer keeps the code organized. New team members can understand the data flow quickly because there are no hidden abstractions. Everything is explicit, just like good Go code should be.
Give this pattern a try. Start with a single endpoint. Experience the confidence that comes from compile-time type safety from your database all the way to your HTTP response. I think you’ll appreciate the simplicity and robustness it brings to your workflow.
If this approach resonates with you, or if you have your own tips for building type-safe services in Go, I’d love to hear about it. Please share your thoughts in the comments below. If you found this useful, consider liking and sharing this article with other developers who might benefit from a cleaner, safer way to build web services. Let’s build more reliable software, together.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva