myapp: a Go REST service skeleton that carries the whole cloud native toolchain
A production-grade microservice boilerplate featuring GORM, Goose, Swag, Zerolog, Validator.v10, GitOps, Kubernetes, Kustomize, ArgoCD & Istio.
At a glance
- What is it?
- learning-cloud-native-go/myapp is a teaching repository that builds a containerised REST API in Go with Chi, Zerolog, GORM, Goose, Swag and Validator, then deploys it with Kustomize, ArgoCD and Istio. Its value is the wiring between the pieces rather than the book service, and it is explicitly a course companion rather than a framework.
- Who is it for?
- myapp fits a developer learning how the pieces of a cloud native Go service fit together, because it wires a router, a logger, an ORM with generated repositories, a migration tool, an OpenAPI generator and a GitOps deployment into one repository you can read end to end. It does not fit a team looking for a production framework, since the project describes itself as a series companion and the sample domain is a single books table.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 5 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A book service whose point is the plumbing
The repository is the companion code for a Learning Cloud Native Go series, and the README says so in the first line of the series description: the goal is to build a production ready containerised RESTful API server application using a named set of packages and tools.
The domain is deliberately trivial. There is one table, named books, and five routes for it plus a health endpoint.
| Name | HTTP Method | Route | |-------------|-------------|----------------| | List Books | GET | /v1/books | | Create Book | POST | /v1/books | | Read Book | GET | /v1/books/{id} | | Update Book | PUT | /v1/books/{id} | | Delete Book | DELETE | /v1/books/{id} | | Health | GET | /livez |
That is the point. A books service has one table, one repository, one model, no relationships, and no business rules worth arguing about, so every line of the code you read is about the framework rather than the domain. A project with a real domain would bury the interesting parts.
The request shape is a title, a description, an image URL, a published date and a status, and the response adds a UUID, a created_at and an updated_at. The status field is a smallint in the database and a string in the JSON, and the validation error shape is a map of field name to message.
The idea is that if you can trace a request from the route through validation, through the repository, into PostgreSQL and back out as a generated OpenAPI response, you have understood the shape of a production Go service. What you take away is the wiring.
The library choices, and the reason each one is in the list
The series description names each library and gives a one line reason, and the reasons are the interesting part.
The HTTP layer is the Go standard library net/http, described as the most idiomatic way to write web API applications in Go, with Chi on top of it, described as the most idiomatic router with middleware and route groups support. Logging is Zerolog, a zero allocation JSON logger, and the note says it is faster than slog. The migration tool is Goose, the database migration CLI builder library with a light dependency. The repository layer is Gorm CLI, which generates Go generics based, type safe repository functions with no runtime wrappers.
That last one is the most consequential decision and worth spelling out. Most GORM usage wraps a runtime call in a hand written function, which means the compiler cannot check the query or the result type. Generating the repository functions instead means the query lives in generated Go code with real types, and the trade is that you regenerate rather than hand edit.
API documentation is Swag for OpenAPI generation, with Validator v10 for validation, and the description calls them the most prominent OpenAPI specification generator and validation library in Go. Around them the operational layer is GitHub Actions for linters, tests, and building and pushing production images to the registry, and GitOps with ArgoCD to automate declarative environment orchestration and application lifecycle management.
The go.mod pins the versions, and they are all current: chi 5.4.0, zerolog 1.35.1, goose 3.29.0, gorm 1.31.2 with the postgres driver at 1.6.3, gorm cli 0.2.4, validator 10.30.5, swag 0.22.3, pgx 5.11.0, uuid 1.6.0, cobra 1.9.1, and the caarlos0/env package for configuration. The module is named myapp, which tells you the import paths are local rather than published, another sign that this is a template you copy rather than a library you depend on.
The Justfile is the development loop, and it is a small file
Instead of a Makefile, the repository uses a Justfile, and the command list is the fastest way to understand the intended workflow.
MYAPP
help # List available commands
install # Install development tools
app # Run server app
migration cmd="up" # Run DB migration CLI (defaults to up)
build # Run docker compose build
up cmd="" # Run docker compose up
down # Run docker compose down
lint # Run lints using gofumpt, go vet, staticcheck and govulncheck
test # Run tests
gen # Run go generate for all packages
apidoc # Generate openapi.yaml
repos # Generate gorm repositories using gorm cliSeveral of those commands are code generation, and that is the loop the repository is teaching. `gen` runs go generate across the packages, `repos` regenerates the GORM repositories, and `apidoc` regenerates openapi.yaml. The repository root already contains an openapi.yaml, so the generated artifact is committed rather than built at deploy time, which means a reviewer can see the API surface in a diff.
The lint command is worth noting for the specific tools it names: gofumpt for formatting, go vet, staticcheck and govulncheck. That last one is a vulnerability scanner over the dependency graph, and including it in the default lint set rather than as a separate CI step is the kind of habit that distinguishes a boilerplate meant to be used from one meant to be read.
There is a test target too, and the sample request log at the end of the README shows the app and the database container starting together, with the database reporting that the system is ready to accept connections and the app logging alongside it.
Two Dockerfiles, and the 30 MB image
The container story is the most concrete number in the repository, and it is the reason the development and production images are different.
| Environment | Go Image Type | Go Image Size | Postgres Image Type | Postgres Image Size | |----------------|------------------------------------|---------------|---------------------|---------------------| | Development | golang:1.27-alpine | ~ 800 MB | postgres:18-alpine | ~ 300MB | | Production | distroless/static-debian13:nonroot | ~ 30 MB | | |
The development Dockerfile is the golang:1.27-alpine base with gcc, musl-dev and tzdata installed, a go mod download step before the source is copied so the dependency layer caches, and then two build commands. The first produces the app binary with the -w and -s linker flags, and the second builds a migration binary behind an embed build tag, so both binaries ship in one image.
RUN go build -ldflags '-w -s' -a -o ./bin/app ./cmd/app \
&& go build -tags=embed -ldflags '-w -s' -a -o ./bin/migration ./cmd/migrationThe compose file then runs the migration binary before the app, which is the detail that makes the local stack match a real deployment.
command: [ "sh", "-c", "/myapp/bin/migration up && /myapp/bin/app" ]The compose file also defines a Postgres service on postgres:18-alpine with a healthcheck using pg_isready, a three second interval, a five second timeout and five retries, and the app service declares depends_on the database with condition service_healthy. That ordering matters, because without it the app starts, the migration fails to connect, and the restart policy turns a race into a loop.
The production image is a separate prod.Dockerfile using distroless/static-debian13:nonroot, which is where the 800 MB to 30 MB difference comes from. A distroless image has no shell and no package manager, so the compose file's sh -c entrypoint would not work there, which is exactly why the production path needs its own file rather than a tag.
Column Tetris, which is the one database lesson that is not obvious
The database section is short by design, with a single table, and then it contains the most useful piece of advice in the whole repository.
The table has eight columns. created_at and updated_at are TIMESTAMPTZ and not null, id is a UUID and is the primary key, published_date is a DATE and not null, status is a SMALLINT and not null, title is TEXT and not null, and description and image_url are TEXT and nullable.
The important note is the one about column ordering. The README explains that for high traffic systems with very large PostgreSQL tables containing millions or billions of rows, arranging fixed width columns by decreasing alignment requirements can reduce tuple alignment padding and potentially minimise row and storage size. The technique is called Column Tetris, and the ordering is by decreasing alignment: 8 byte types such as bigint, double precision, timestamp, timestamptz, time and interval, then 4 byte types such as integer, real, uuid and date, then 2 byte types such as smallint, then 1 byte types such as boolean, and finally the variable width types, numeric, text, varchar and bytea, at the end.
The README also gives you permission to ignore it, with a caveat. However, it is ok to follow a more readable column format when your table schema changes frequently.
That is a genuinely mature piece of advice, because it says the optimisation has a cost in readability and tells you when the cost is not worth paying. Postgres stores rows with padding so that every row in a tuple has the same alignment, and if a smallint sits before an eight byte timestamp, the padding grows. On a table with billions of rows that is real storage.
The table as shipped does follow the rule: the two timestamptz columns come first, then the uuid and date, then the smallint, and the text columns last. Reading the note and then reading the schema is instructive in itself.
GitOps with ArgoCD and Istio, in a k8s directory
The deployment story sits in the k8s/ directory at the repository root, and the approach is declared rather than implied: GitOps with ArgoCD to automate declarative environment orchestration and application lifecycle management. The topics list adds Istio and the repository description names ArgoCD and Istio alongside Kustomize.
So the model is that the desired state of every environment lives in Git, and a controller in the cluster reconciles the cluster with it. A change to a deployment, a configuration value or a feature flag is a pull request, not a kubectl command, and the audit trail is the commit history.
Istio sits on top of that as the service mesh layer, which means the traffic between your service and its dependencies is handled by sidecar proxies rather than by the application. For a learning repository, that is a deliberate choice to show the mesh in the same tree as the app that will run inside it, and it is also a significant operational dependency, since a mesh changes how failures propagate and how you debug a request that is not arriving.
The image registry side is handled by GitHub Actions, which the series description says runs the linters and tests and builds and pushes production images to the registry. Combined with ArgoCD, the promotion path is the standard one: build once, push the image, update the tag in the manifest, let ArgoCD sync it.
What the README does not include is any of the operational detail that usually comes with such a stack, such as how the Istio and ArgoCD components themselves are installed, what the ingress and mesh configuration looks like, or how the environments are separated. The k8s/ directory is there to read, and the journal linked from the homepage at learning-cloud-native-go.github.io/journal is where the written parts of the series live.
Editorial conclusion
myapp fits a developer learning how the pieces of a cloud native Go service fit together, because it wires a router, a logger, an ORM with generated repositories, a migration tool, an OpenAPI generator and a GitOps deployment into one repository you can read end to end. It does not fit a team looking for a production framework, since the project describes itself as a series companion and the sample domain is a single books table. Check three things before you adopt it. Check the domain, because the whole schema, the endpoints and the OpenAPI document are about books, and the value is in the plumbing rather than the model. Check the toolchain, because the Justfile drives the development loop and the k8s directory assumes ArgoCD and Istio are what you deploy with. And check the version, because the module targets Go 1.27 and the release list shows steady movement through v3.5.1, so a fork will need updating. The last push was on 2026-10-13, the newest release is v3.5.1 from 2026-08-30, and the license is MIT.
Frequently asked questions
What is the myapp repository in learning-cloud-native-go?
It is the companion code for a Learning Cloud Native Go series, building a production ready containerised RESTful API in Go using net/http, Chi, Zerolog, Goose, Gorm CLI, Swag, Validator v10, GitHub Actions and ArgoCD.
How do I run the myapp development environment?
The Justfile drives it. just install installs the development tools, just up runs docker compose up, just app runs the server, just migration runs the database migration CLI, and just down stops the stack.
Which Go libraries does myapp use and why?
The standard library net/http with Chi as the router, Zerolog as a zero allocation JSON logger, Goose for database migrations, Gorm CLI to generate generics based type safe repository functions with no runtime wrappers, and Swag with Validator v10 for OpenAPI generation and request validation.
What is Column Tetris and why does the myapp README mention it?
It is the practice of ordering fixed width PostgreSQL columns by decreasing alignment requirement, 8 byte types first down to 1 byte types, with variable width types last, to reduce tuple alignment padding and storage size. The README notes it is worth following for very large tables but says a more readable format is fine when the schema changes often.
How does myapp deploy to Kubernetes?
With GitOps and ArgoCD, described as automating declarative environment orchestration and application lifecycle management, using Kustomize and Istio in the k8s directory, with GitHub Actions building and pushing production images to the registry.
What license is myapp released under?
MIT, with a LICENSE file at the root of the repository. The newest listed release is v3.5.1 from 2026-08-30, and the last push was on 2026-10-13.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/learning-cloud-native-go-myapp)Was this useful?