Project Layout Idioms
src/ vs cmd/ + internal/
Section titled “src/ vs cmd/ + internal/”TypeScript projects almost universally put source code in a src/ directory. Beyond that, the layout is up to you — NestJS has its own conventions, Next.js has its own, and a plain Express project might look completely different.
Go has a well-established community standard that is worth learning early. Most Go projects you encounter on GitHub follow the same skeletal structure:
flowchart TD ROOT["myapp/"] --> CMD["cmd/"] CMD --> CMDAPP["myapp/"] CMDAPP --> MAIN["main.go (entry point binary)"] ROOT --> INT["internal/"] INT --> HAND["handler/user.go"] INT --> SVC["service/user.go"] INT --> REPO["repository/user.go"] ROOT --> PKG["pkg/"] PKG --> LOG["logger/logger.go"] ROOT --> MOD["go.mod"] ROOT --> SUM["go.sum"]
// Typical TypeScript / Node projectsrc/ main.ts // entry point modules/ users/ users.controller.ts users.service.ts users.repository.ts shared/ logger.ts utils.tspackage.jsontsconfig.json// Idiomatic Go projectcmd/ myapp/ main.go // entry pointinternal/ handler/ user.go service/ user.go repository/ user.gopkg/ logger/ logger.gogo.modgo.sumcmd/ — one directory per binary
Section titled “cmd/ — one directory per binary”The cmd/ directory holds the entry points for your program(s). Each sub-directory name becomes a binary. If you have a web server and a background worker:
cmd/ server/ main.go # go build ./cmd/server → produces ./server binary worker/ main.go # go build ./cmd/worker → produces ./worker binaryThis is useful because a single Go module can produce multiple binaries. There is no equivalent convention in Node — you would typically use separate package.json workspaces or separate repositories.
internal/ — compiler-enforced privacy
Section titled “internal/ — compiler-enforced privacy”The internal/ directory is enforced by the Go compiler itself. Code inside internal/ can only be imported by code in the parent tree of that internal/ directory.
# Given this layout:myapp/ internal/ service/ user.go # package service
# This import is ALLOWED (same module):import "myapp/internal/service" // from cmd/myapp/main.go ✓
# This import is BLOCKED (different module):import "myapp/internal/service" // from github.com/other/proj ✗# compiler error: use of internal package not allowedThis gives you a hard visibility boundary that TypeScript cannot enforce — even private class members are a convention enforced only by tsc, not the runtime.
pkg/ — optional shared code
Section titled “pkg/ — optional shared code”pkg/ is for code you explicitly want to be importable by external consumers. Think of it as your public API surface — the opposite of internal/. Many teams skip pkg/ entirely and put shared utilities directly in named packages at the root.
Package naming conventions
Section titled “Package naming conventions”Go package names are short, lowercase, singular nouns — no underscores, no camelCase.
# Goodpackage handlerpackage servicepackage repositorypackage logger
# Avoidpackage userHandlers # camelCasepackage user_service # underscorespackage handlers # plural (minor, but common to see singular)The package name is what callers write: handler.NewUser(...), service.CreateOrder(...). Keep it short so the fully-qualified name reads naturally.
There is no runnable snippet here — this is all filesystem and compiler behaviour. Try creating the layout above in your terminal with
mkdir -p cmd/myapp internal/service && touch cmd/myapp/main.go internal/service/user.go go.mod.