# Goapp is a directory layout for Go services, with internal/ doing the real work

> Goapp is an opinionated guideline for structuring a Go web application or service, written by someone with nine years of building them and shaped by domain driven design and Clean Architecture. The structure is the deliverable: an internal tree that separates configuration, the public API surface, domain packages, shared plumbing and the process entrypoint, and the internal directory is what makes the boundaries enforceable by the compiler.

**naughtygopher/goapp** — An opinionated guideline to structure & develop a Go web application/service

- Repository: https://github.com/naughtygopher/goapp
- Stars: 1,077 · Forks: 61
- Language: Go
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/naughtygopher-goapp

## A layout, not a framework

Goapp is an opinionated guideline to structure a Go web application or service, and the author is explicit that it could be extended for any type of application. The opinions were formed over a span of more than nine years building web applications and services with Go, with the aim of implementing domain driven design and Clean Architecture.

The important thing to understand is that there is nothing to import. No dependency, no base type to embed, no runtime. What you get is a directory tree, a set of naming conventions, and prose explaining why each directory exists. You take the tree, delete the example code, and write your own.

The example the structure is explained through is a note taking web application with hardly any features implemented, which is a good choice for a guideline. A domain with a user and a note is small enough to read in one sitting and still has the two domain units, a data store, an API and a subscriber, which is enough to show where each kind of code belongs.

There is a second example, the note taking application's own build, and the top level of the repository is the worked result. The module is github.com/naughtygopher/goapp, it targets Go 1.27, and the licence is MIT. The last push was on 2026-09-06, the project is not archived, and it is listed in the awesome-go tutorials collection. The repository publishes no GitHub releases, which is consistent with a document that is versioned by its commits rather than by tags.

## The internal directory is what makes the boundaries real

The tree is the specification, and the top level of it looks like this.

```bash
├── cmd
│   ├── server
│   │   ├── grpc
│   │   │   └── grpc.go
│   │   └── http
│   │       ├── handlers.go
│   │       ├── handlers_usernotes.go
│   │       ├── handlers_users.go
│   │       ├── http.go
```

Underneath that sit docker/ with a Dockerfile and a compose file, internal/ with api, configs, pkg, usernotes and users, a separate lib/ directory, main.go, inits.go and shutdown.go at the root, and schemas/ with the SQL files.

The one piece of Go knowledge the guideline insists on is the special internal directory name, which has existed since Go 1.4. Inside it, any exported name or entity can only be consumed within its immediate parent or by other packages within the internal directory. That is the whole enforcement mechanism, and it is a compiler rule rather than a convention, which is the difference between a boundary that survives a deadline and one that does not.

The guideline states that it works for Go 1.4 and later precisely because of that, which is a good way to understand how old the idea is and how little it depends on the language getting fancier.

## Everything outside enters through internal/api

The API package is the one with the strongest rule. It is supposed to have all the APIs exposed by the application, and a dedicated API package is created to standardise the functionality when there are different kinds of services running, for example an HTTP and a gRPC server, or a Kafka and a PubSub subscriber. In those cases the respective handler functions call api.<Method name>, which gives a guarantee that all your APIs behave exactly the same without accidental inconsistencies across different I/O methods.

The second benefit is stated just as plainly. It helps consolidate which functionalities are expected to be exposed outside of the application via API. There could be a variety of exported functions in the domain packages, which are not meant to communicate with anything outside the application but rather to be used among other domain packages.

So the rule is simple. A domain package exports what it needs for other domain packages to use, and anything that leaves the process goes through the api package. When you add a second transport, a gRPC service alongside HTTP, or a second subscriber, the handlers in each of them call the same api methods, and the behaviour cannot drift between them because there is only one implementation.

One exception is drawn explicitly. Middleware handling stays at the internal/server layer, for example access log and authentication, and even though it could be brought into the api package, it does not make much sense because middleware is mostly dependent on the server and handler implementation, for example the HTTP method and the path.

## A domain package owns its datastore behind its own interface

The users package is the worked example of a domain unit, and the rule for it is that all the actual user related business logic is implemented there, for example creating a user after cleaning up the input, validating it, and then storing it in a persistent datastore.

The datastore interaction has its own file. The store_postgres.go in that package is where all the direct interactions with the datastore go, and there is an interface which is unique to the users package, used to handle dependency injection and dependency inversion elegantly. The file naming convention is to have the word store at the beginning, suffixed with the database name, though the author also considers it fine to name it after a logical group such as store_registration or store_login, especially when there is a lot of database or storage related code to cram into a single file.

Construction follows the same pattern in every package. A NewService or New function is created in each package, which initialises and returns the respective package's feature implementor, in the case of the users package the Users struct, and the name NewService makes sense in most cases and reduces the burden of thinking of a good name. The Users struct holds all the dependencies required for implementing the features the package provides.

That last point is the piece that makes the whole layout work. Dependencies are declared where they are used, as a struct field, and the wiring happens in one place at the top level, in inits.go, rather than being spread through the call graph.

## Configs, shared plumbing and the process entrypoint

The configs package gets a section of its own because the author defends it against the obvious objection. Creating a dedicated configs package might seem like an overkill, but it makes things easier. The argument runs through a small story: in the app you see the HTTP configs are hardcoded and returned, later you decide to consume them from environment variables, and all you do is update the configs package. Further down the line you might decide to introduce something like etcd, and then you define the dependency in Configs and update the functions accordingly. It is another separation of concern package, to try and keep main tidy.

The pkg directory holds the shared infrastructure that is not domain logic: apm with its grpc, http, meter, prometheus and tracer files, logger with a default.go and a logger.go, postgres, and sysignals.

The lib/ directory is a separate Go module, with its own go.mod and go.sum, containing a package called goapp. That is a deliberate boundary for shared code that has to be versioned independently, and the presence of a second go.mod inside one repository is worth noticing if you are copying the layout, because it changes how you build and how you import.

At the root sit main.go, inits.go and shutdown.go, and the schemas/ directory holds functions.sql, user_notes.sql and users.sql, so the database schema is versioned next to the code that uses it rather than in a separate migrations repository.

## Observability is in the tree, not bolted on

The dependency list is where the guideline's production instincts show. The module requires the core OpenTelemetry packages at version 1.46.0, with the gRPC and net/http instrumentation packages, the B3 propagator, the OTLP trace exporters over both gRPC and HTTP, the Prometheus exporter, the stdout metric and trace exporters, and the metric and trace SDKs. On the metrics side there is the Prometheus client at 1.24.1, and there is otelpgx for the database.

The tree itself contains an apm package with apm.go, grpc.go, http.go, meter.go, prometheus.go and tracer.go, so the instrumentation is a package you wire in rather than calls scattered through the handlers. The README also has a section on integrating with ELK APM, which is what the trace and metric exporters are for.

The rest of the dependency list is a good picture of a service that talks to Postgres and streams from Kafka. Squirrel is the SQL query builder, pgx/v5 is the Postgres driver, uuid generates identifiers, and there is a Kafka subscriber under cmd/subscribers/kafka. The gRPC and HTTP servers are siblings under cmd/server, and each has its own handlers file, with separate handler files for users and for usernotes.

The one dependency that reveals the author is not a framework minimalist is webgo/v7, the HTTP router, along with naughtygopher/errors and naughtygopher/proberesponder, which are the author's own libraries. That is worth noting when you adopt the layout: some of the structure assumes these building blocks are present.

## The guideline admits its own exceptions

Two admissions in the README are more useful than another feature list, because they tell you where the opinion stops.

The first is the post script. This guideline is not directly applicable for an independent package, as their primary use of such a package is to be consumed in other applications, and in such cases having most or all of the package code in the root is probably the best way of doing it. A library and a service have genuinely different structures, and the author says so rather than pretending otherwise.

The second is the test coverage discussion, which starts with the observation that 100 percent coverage sounds very nice but might not always be practical, or at times not even possible. The internal/users_test package in the tree is where those discussions live.

The rest of the table of contents covers the parts of the layout that this article does not cover in full: the pkg datastore and logger packages, the HTTP server and its templates, the vendor directory, docker, the schemas, main.go, error handling, the dependency flow between the layers, and the ELK APM integration. A note at the end of the list closes the document.

Taken together, this is a guideline rather than a framework, and its value is in the parts that are enforced by the compiler and the parts that are argued in prose. The tree you can copy in an afternoon. The habit of routing every external call through one api package is the part that takes a team a release cycle to adopt, and the part that pays off when the second transport arrives.

## Conclusion

Goapp fits a Go team that has outgrown a flat main package and wants the boundaries of a domain driven layout without adopting a framework, since it is a tree and a set of conventions rather than a library. It is the wrong tool for an independent library, and the README says so explicitly, since a package consumed by other applications is usually better with most of its code in the root. Verify first that your team accepts the opinionated parts, particularly the rule that every external entry point calls through internal/api, before you move an existing service onto this layout.

## FAQ

### What is Goapp?

It is an opinionated guideline to structure a Go web application or service, written by someone with more than nine years of experience and shaped by domain driven design and Clean Architecture. The structure is explained through a note taking web application, and the guide works with Go 1.4 and later.

### Why does the Goapp layout use an internal directory?

Because internal is a special directory name in Go, where any exported name or entity can only be consumed within its immediate parent or by other packages within the internal directory. That makes the boundaries enforceable by the compiler rather than by convention.

### What belongs in the internal/api package?

All the APIs exposed by the application. When there are different kinds of services running, such as an HTTP and a gRPC server or a Kafka and a PubSub subscriber, the handlers call api.<Method name>, which guarantees the APIs behave the same across I/O methods. Middleware stays at the server layer because it depends on the handler implementation.

### How does a domain package handle its database?

Direct datastore interaction goes in a file named with the store prefix and the database name, such as store_postgres.go, and the package defines its own interface for dependency injection and dependency inversion. A NewService or New function returns the package's implementor struct, which holds the dependencies it needs.

### Is the Goapp layout suitable for a library?

The README says it is not directly applicable for an independent package, since such a package is mainly consumed by other applications, and in that case having most or all of the code in the root is probably the best way of doing it. The licence is MIT and the last push was on 2026-09-06.

## Sources

- [Issues](https://github.com/naughtygopher/goapp/issues)
- [License: MIT](https://github.com/naughtygopher/goapp/blob/master/LICENSE)
- [naughtygopher/goapp on GitHub](https://github.com/naughtygopher/goapp)
- [README](https://github.com/naughtygopher/goapp/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/naughtygopher-goapp
