Skip to content
Back to skills

577 Project Structure 22d883f8

ASecurity

Best for small applications, CLIs, microservices with fewer than ~10 handlers, and projects where a single developer or small team owns the entire codebase.

  • 9 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added June 1, 2026
devopsgosqlapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned June 1, 2026

npx -y skills add tools-only/X-Skills --skill 577-project-structure_22d883f8 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 577 Project Structure 22d883f8?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 577 Project Structure 22d883f8
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-577-project-structure-22d883f8/badge)](https://www.skillsdirectory.com/skills/tools-only-577-project-structure-22d883f8)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# Go Project Structure

## Flat Structure

Best for small applications, CLIs, microservices with fewer than ~10 handlers, and projects where a single developer or small team owns the entire codebase.

```text
myapp/
├── main.go
├── server.go
├── handlers.go
├── middleware.go
├── models.go
├── store.go
├── server_test.go
├── handlers_test.go
└── go.mod
```

### When to Use

- CLI tools and small utilities
- Single-purpose microservices (one bounded context)
- Prototypes and proofs of concept
- Fewer than ~10 HTTP handlers
- One or two developers working on the codebase

### Benefits

- Zero cognitive overhead for navigation -- everything is in one place
- No circular dependency issues (single package)
- Easy to refactor -- just move functions between files
- `go test ./...` covers everything in one pass
- New contributors can understand the layout immediately

### File Responsibilities

| File | Contains |
|------|----------|
| `main.go` | `func main()`, wiring, configuration loading, `run()` function |
| `server.go` | `Server` struct, `NewServer()`, `routes()`, `ServeHTTP()` |
| `handlers.go` | All HTTP handler methods on `Server` |
| `middleware.go` | Middleware functions (`requestLogger`, `authenticate`, etc.) |
| `models.go` | Domain types, request/response structs |
| `store.go` | Database access layer (queries, store struct) |

For very small apps, `server.go` and `handlers.go` can be the same file.

### Example: Flat Server

```go
// main.go
package main

import (
    "context"
    "database/sql"
    "log/slog"
    "os"
)

func main() {
    if err := run(context.Background()); err != nil {
        slog.Error("application error", "err", err)
        os.Exit(1)
    }
}

func run(ctx context.Context) error {
    db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        return fmt.Errorf("opening db: %w", err)
    }
    defer db.Close()

    srv := NewServer(db, slog.Default())
    // ... start and graceful shutdown ...
    return nil
}
```

```go
// server.go
package main

type Server struct {
    db     *sql.DB
    logger *slog.Logger
    router *http.ServeMux
}

func NewServer(db *sql.DB, logger *slog.Logger) *Server {
    s := &Server{db: db, logger: logger, router: http.NewServeMux()}
    s.routes()
    return s
}

func (s *Server) routes() {
    s.router.HandleFunc("GET /api/items", s.handleListItems)
    s.router.HandleFunc("GET /api/items/{id}", s.handleGetItem)
    s.router.HandleFunc("POST /api/items", s.handleCreateItem)
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    s.router.ServeHTTP(w, r)
}
```

---

## Modular / Domain-Driven Structure

For larger applications with multiple bounded contexts, multiple teams, or significant growth expected.

```text
myapp/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── user/
│   │   ├── handler.go
│   │   ├── service.go
│   │   ├── store.go
│   │   ├── model.go
│   │   └── handler_test.go
│   ├── order/
│   │   ├── handler.go
│   │   ├── service.go
│   │   ├── store.go
│   │   ├── model.go
│   │   └── handler_test.go
│   └── platform/
│       ├── middleware/
│       │   ├── auth.go
│       │   └── logging.go
│       ├── database/
│       │   └── postgres.go
│       └── config/
│           └── config.go
├── migrations/
│   ├── 001_create_users.up.sql
│   └── 001_create_users.down.sql
├── go.mod
└── go.sum
```

### Directory Conventions

#### `cmd/`

Entry points for the application. Each subdirectory produces one binary.

```text
cmd/
├── server/
│   └── main.go       # HTTP server
├── worker/
│   └── main.go       # Background job processor
└── migrate/
    └── main.go       # Database migration tool
```

Each `main.go` is the composition root: it reads config, creates dependencies, wires them together, and starts the program. Keep `main.go` small -- delegate to a `run()` function that returns an error.

#### `internal/`

The `internal/` directory is enforced by the Go toolchain. Code inside `internal/` cannot be imported by external modules. Use it for all application-specific code.

```go
// This import is only allowed from within the same module:
import "myapp/internal/user"
```

This gives you the freedom to refactor internal packages without worrying about breaking external consumers.

#### Domain Packages (`internal/user/`, `internal/order/`)

Each domain package owns its:
- **Models** -- domain types and validation
- **Store** -- database queries, implements a store interface
- **Service** -- business logic, orchestrates store calls
- **Handler** -- HTTP handlers, request parsing, response writing

```go
// internal/user/service.go
package user

type Service struct {
    store Store
}

type Store interface {
    GetByID(ctx context.Context, id string) (*User, error)
    Create(ctx context.Context, u *User) error
    List(ctx context.Context, limit, offset int) ([]User, error)
}

func NewService(store Store) *Service {
    return &Service{store: store}
}

func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
    if id == "" {
        return nil, fmt.Errorf("user id is required")
    }
    return s.store.GetByID(ctx, id)
}
```

#### `internal/platform/`

Shared infrastructure code that is not domain-specific:
- `middleware/` -- HTTP middleware (logging, auth, CORS)
- `database/` -- connection helpers, migration runners
- `config/` -- configuration loading and validation

Platform packages are imported by domain packages and `cmd/`, but never import domain packages.

### Package Design Principles

**Dependencies flow inward.** Domain packages should not import other domain packages. If `order` needs user data, it defines its own interface:

```go
// internal/order/service.go
package order

type UserLookup interface {
    GetByID(ctx context.Context, id string) (*User, error)
}

type Service struct {
    store      Store
    userLookup UserLookup
}
```

The `cmd/server/main.go` wires the `user.Service` (which satisfies `order.UserLookup`) into the order service.

**Avoid circular dependencies.** If package A imports package B, package B cannot import package A. This is a compile error in Go. Solutions:
1. Extract shared types into a separate package (e.g., `internal/domain`)
2. Use interfaces at the consumer side
3. Merge the packages if they are tightly coupled

**Keep packages focused.** A package named `utils` or `helpers` is a code smell. If a function doesn't belong to a domain, it belongs in `platform/` with a descriptive package name.

**Export only what is needed.** Start with unexported types and functions. Export them only when another package needs access.

### Wiring in main.go

```go
// cmd/server/main.go
package main

import (
    "myapp/internal/order"
    "myapp/internal/platform/config"
    "myapp/internal/platform/database"
    "myapp/internal/user"
)

func run(ctx context.Context) error {
    cfg, err := config.Load()
    if err != nil {
        return fmt.Errorf("loading config: %w", err)
    }

    db, err := database.Open(cfg.DatabaseURL)
    if err != nil {
        return fmt.Errorf("opening db: %w", err)
    }
    defer db.Close()

    // Build dependency graph
    userStore := user.NewPostgresStore(db)
    userService := user.NewService(userStore)

    orderStore := order.NewPostgresStore(db)
    orderService := order.NewService(orderStore, userService)

    // Build server
    mux := http.NewServeMux()
    user.RegisterRoutes(mux, userService)
    order.RegisterRoutes(mux, orderService)

    // ... start HTTP server with graceful shutdown ...
    return nil
}
```

### Route Registration in Domain Packages

Each domain package provides a `RegisterRoutes` function:

```go
// internal/user/handler.go
package user

func RegisterRoutes(mux *http.ServeMux, svc *Service) {
    h := &handler{svc: svc}
    mux.HandleFunc("GET /api/users", h.list)
    mux.HandleFunc("GET /api/users/{id}", h.get)
    mux.HandleFunc("POST /api/users", h.create)
}

type handler struct {
    svc *Service
}

func (h *handler) get(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    u, err := h.svc.GetUser(r.Context(), id)
    // ...
}
```

---

## Migration Signals: Flat to Modular

Move from flat to modular when you notice:

1. **File length** -- `handlers.go` exceeds ~500 lines or contains unrelated handlers
2. **Naming collisions** -- You prefix functions like `userGetHandler`, `orderGetHandler` to avoid confusion
3. **Multiple developers** -- Merge conflicts in shared files become frequent
4. **Distinct domains** -- The application clearly has separate bounded contexts (users, orders, billing)
5. **Separate deployment needs** -- You want a CLI tool and an HTTP server from the same codebase (`cmd/server/`, `cmd/cli/`)
6. **Test isolation** -- You want to test one domain without loading all the others

### How to Migrate

1. Create `cmd/server/main.go` and move wiring code there
2. Create `internal/` and make one domain package for the most independent domain
3. Move its models, handlers, store, and tests into the new package
4. Update imports in `main.go`
5. Repeat for each domain
6. Extract shared infrastructure into `internal/platform/`

Migrate incrementally. Do not restructure everything in one commit.

Files in this skill

  • README.md1 KB
  • skill.md9.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.