01 - Project Structure & Setup
📋 Jump to TakeawaysGo doesn't enforce a project layout. That's a feature, not a bug, but it means you have to make decisions. Here's a practical layout that scales without over-engineering.
Initialize the Project
mkdir bookmarks && cd bookmarks
go mod init bookmarksThat's it. One command, one file. No scaffolding tools, no generators.
Flat Start, Grow When Needed
Start flat. Don't create 10 directories on day one. Add structure when the code tells you to: when a file gets too long, when you need to share code between packages, when things get hard to find.
bookmarks/
├── go.mod
├── main.go
├── handler.go ← HTTP handlers
├── model.go ← data types
├── store.go ← database access
└── config.go ← configuration loadingThis is fine for a small service. Everything in package main. The file names tell you where to look. No ceremony. This is what we'll use in this course.
Keep main.go thin. It wires things together and starts the server. No business logic:
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
fmt.Printf("listening on :%s\n", port)
log.Fatal(http.ListenAndServe(":"+port, mux))
}Run it:
go run .Visit http://localhost:8080/health and you should see ok.
Growing Beyond Flat
When the project gets bigger — multiple binaries, shared utilities, code you want to hide from external importers — the Go convention is cmd/ for entry points and internal/ for private packages:
bookmarks/
├── go.mod
├── cmd/
│ └── server/
│ └── main.go ← entry point, wires things together
├── internal/
│ ├── bookmark/
│ │ ├── handler.go
│ │ ├── model.go
│ │ └── store.go
│ ├── config/
│ │ └── config.go
│ └── middleware/
│ └── logger.gointernal/ is special in Go — code inside it can only be imported by code in the parent module. The Go toolchain enforces this at compile time. Other projects can't import your internal packages, so you're free to refactor without breaking anyone.
cmd/ holds your executables. If you later add a CLI tool or a migration script, each gets its own subdirectory under cmd/.
Multiple Domains
When your project has multiple domains (users, notes, bookmarks, tags), organize internal/ by domain, not by layer. Each domain gets its own package:
project/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── user/
│ │ ├── handler.go
│ │ ├── model.go
│ │ └── store.go
│ ├── bookmark/
│ │ ├── handler.go
│ │ ├── model.go
│ │ └── store.go
│ └── middleware/
│ └── logger.goEach domain package exposes a handler struct and a method to register its routes:
// internal/bookmark/handler.go
package bookmark
type Handler struct {
store *Store
}
func NewHandler(store *Store) *Handler {
return &Handler{store: store}
}
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /bookmarks", h.handleList)
mux.HandleFunc("POST /bookmarks", h.handleCreate)
}Then main.go wires them together:
// cmd/server/main.go
package main
import (
"log"
"net/http"
"myproject/internal/bookmark"
"myproject/internal/user"
)
func main() {
db := openDB()
defer db.Close()
mux := http.NewServeMux()
bookmarkHandler := bookmark.NewHandler(bookmark.NewStore(db))
bookmarkHandler.RegisterRoutes(mux)
userHandler := user.NewHandler(user.NewStore(db))
userHandler.RegisterRoutes(mux)
log.Fatal(http.ListenAndServe(":8080", mux))
}Each domain owns its routes. main.go just calls RegisterRoutes for each one.
Don't do this — grouping by layer spreads every feature across multiple packages:
internal/
├── handler/
│ ├── user.go
│ └── bookmark.go
├── model/
│ ├── user.go
│ └── bookmark.go
├── store/
│ ├── user.go
│ └── bookmark.goWith the domain approach, everything about bookmarks lives in internal/bookmark/. You can understand one feature without reading the rest of the codebase.
What About pkg/?
You'll see many Go projects with a pkg/ directory alongside cmd/ and internal/. pkg/ signals that the code inside is meant to be imported by other projects. If you're building a library or shared utilities across repos, that's where they go. For a single-project API like ours, you won't need it. Most application code belongs in internal/.
Start flat. Add cmd/ when you have multiple binaries. Add internal/ when you want to hide implementation details. Add pkg/ when you have code that other projects should import.
One go.mod, one binary, flat layout. That's our starting point. We'll add structure as the project grows, not before.
Don't Over-Engineer
The Go community has a saying: "A little copying is better than a little dependency." The same applies to structure. A little duplication in a flat layout is better than a premature abstraction in a deep directory tree.
If you're spending more time deciding where a file goes than writing the code in it, your structure is too complex.
Key Takeaways
go mod initand a flat layout is all you need to start- Organize
internal/by domain (internal/user/), not by layer (internal/handler/) internal/hides code from external importerscmd/is for multiple binaries,pkg/is for shared importable code- Keep
main.gothin — wire things up, start the server, nothing else - Add structure when the code tells you to, not before