1. Why you need a 5–10 minute demo scenario
If you have ever been shown a project in the style of “now I’ll explain everything,” and 20 minutes later you were still staring at the folder structure — you know the pain. A demo scenario is a way to avoid turning a results showcase into a code tour (we love code anyway, but not everyone is required to share that feeling). The “5–10 minutes” format enforces discipline: you show only what delivers value and trust.
An important idea: a good demo is not “what we have,” but “what the user can do and what they can rely on.” That is why demos are almost always built in the order behavior → contracts → quality signals. Behavior is an observable action (a CLI command or an HTTP request). Contracts are stable rules (stdout/stderr, exit codes, HTTP statuses, error envelope). Quality signals are “why you can trust this” (stable output, tests, no leaking of internal errors, request_id, etc.).
To keep this concrete, in this lecture we will assume we have a training application Tasker: a task manager with a CLI and an HTTP API. We will not “add more functionality” (the course is already being finalized); we will learn how to present what you have already built.
Mini diagram: what exactly we show
flowchart TD
A[Demo starts with behavior] --> B[CLI: commands and exit codes]
B --> C[HTTP: statuses and JSON contract]
C --> D[Quality signals: tests, stability, request_id]
D --> E[Questions / final expectation check]
2. Build the demo as a single story
The most common beginner problem is the desire to show the entire project at once, because “I worked hard.” That is normal (we all want someone to pat our code), but a demo works differently: it is a short story with a beginning, action, and an ending. Like a good joke: if you need 10 minutes to explain it, it is no longer a joke, it is a lecture on the history of humor.
It is convenient to keep the scenario in your head as a mini protocol:
- you have an initial state (an empty task list or a few tasks),
- you perform 2–4 actions,
- each action produces an observable result,
- you show once how the system behaves on an error,
- you finish on a “successful” ending (a task is created/completed/fetched via the API).
To make it easier to repeat, it helps to make the demo into a “timeline.” Not a floating list, but a table — it disciplines both you and the narration.
| Time | Demo step | What we run | What we verify by eye |
|---|---|---|---|
| 0:00–0:30 | Pitch | talk (3–5 sentences) | it is clear what the project is and why it exists |
| 0:30–2:30 | CLI happy path | |
stdout = result, exit code = 0 |
| 2:30–3:30 | CLI error | invalid command/argument | stderr = error/usage, exit code = 2 |
| 3:30–6:30 | HTTP happy path | |
statuses 200/201, JSON is correct |
| 6:30–8:00 | HTTP error | + error envelope |
a unified error format, fields for validation |
| 8:00–10:00 | Quality signals | |
the project feels “grown‑up,” not just “it works” |
In this lecture we will break down how to quickly prepare each part, and what small pieces of code help you hold the contract (so your project does not turn into “it behaves like this today, and tomorrow — we’ll see”).
3. Pitch: a short intro
Funny but true: people most often “fail” the demo not on code and not even on a bug, but on the first explanation. When you start with architecture and folders, the listener does not understand what is going on at all, and the brain goes into “power saving mode.” A pitch is needed so the listener’s brain can conclude: “ok, I understand the problem and the quality criteria.”
The pitch should be short and boring (in a good way). Example for Tasker:
Tasker is a small task manager. It has a CLI and an HTTP API.
In the CLI I will show the stdout/stderr contract and return codes 0/1/2, and in HTTP — statuses and a unified JSON error format.
For unexpected problems, internal details do not leak outside: the 500-class returns a stable message.
Plus I’ll show request_id as a minimal observability signal.
Notice: you name the “quality signals” in advance. This is not bragging, it is navigation: the listener starts looking where they should. And it is easier for you not to drift into “and now I’ll also show this.”
4. CLI demo: stdout/stderr and exit codes
It is convenient to do the CLI demo first: it is quick, local, and understandable. Here you win by immediately showing discipline: stdout is the result, stderr is errors/usage, exit codes are a contract. In the real world, this lets people write scripts and keeps tests stable. In a learning setting, it lets an instructor see that you are not “checking by eye manually,” but designing an interface.
The key trick is to make a central function run(args) and in main() simply call os.Exit(code). It is banal, but very useful: this way you do not sprawl into log.Fatal and “exits from the middle of the program,” and you keep control in one place.
Mini example: CLI skeleton main → run → exit
package main
import (
"os"
)
func main() {
os.Exit(run(os.Args[1:]))
}
This snippet is small, but it sets the style: everything that prints/parses/fails should be inside run.
Mini example: a basic command dispatcher
package main
import (
"fmt"
"os"
)
func run(args []string) int {
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "usage: tasker <command>")
return 2
}
if args[0] == "help" || args[0] == "-h" || args[0] == "--help" {
fmt.Println("usage: tasker <command>") // stdout: this is a normal result
return 0
}
fmt.Fprintln(os.Stderr, "unknown command:", args[0])
return 2
}
Here you demonstrate two contracts at once: help is a success (code 0), and an input error is usage (code 2). This looks like a tiny detail until you start writing tests for the CLI and running commands in bash. Then it suddenly turns out that “tiny details” are the UX.
What to show in a CLI demo
Instead of ten commands, show three:
- “Create a task” (happy path).
- “Show the list” (stable output, sorting/order — if applicable).
- “Break the input” (error, exit code 2, stderr).
An example of “how it might look” (this is not code, but a demo script):
- tasker add -title "Buy milk" → stdout: created task id=1
- tasker list → stdout: a table or lines
- tasker add (without title) → stderr: title is required, exit code 2
At this moment you lock in the contract in words: “I print results to stdout, errors and hints to stderr. For codes: 0 is success, 2 is an argument problem, 1 is an execution error.” And you already look like someone who writes software, not “a set of commands I run by hand.”
5. HTTP demo: statuses and error envelope
After the CLI it is logical to move to HTTP: here you show that the same ideas (contracts, predictability, errors) scale to a network interface. In HTTP it is especially important not to sprawl: if you start talking about middleware, layers, and DI, you will lose your 5–10 minutes before the first request. So keep the focus: request → status → body → error contract.
In this course we agreed in advance on a unified JSON error format: the error envelope. It is like a form for documents: maybe boring, but every client knows where to look.
Mini example: error envelope structures
package api
type ErrorResponse struct {
Error ErrorBody `json:"error"`
}
type ErrorBody struct {
Code string `json:"code"`
Message string `json:"message"`
Fields map[string]string `json:"fields,omitempty"`
}
This structure should be “set in stone”: if you change it every time based on mood, testing and maintaining the API will be painful.
Mini example: a unified writeError without leaking internals
A very important engineering habit: do not return “internal truth” to the outside on 500-class errors. The user needs a stable, safe phrasing. Internal details are for logs. This is directly related to the fact that in Go, errors are values, and you often add context as you move up the stack: there can be a lot of context, and not all of it should be shown to the client.
package api
import (
"encoding/json"
"net/http"
)
func writeError(w http.ResponseWriter, status int, code, msg string, fields map[string]string) {
if status >= 500 {
code = "internal"
msg = "internal error"
fields = nil
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(ErrorResponse{
Error: ErrorBody{Code: code, Message: msg, Fields: fields},
})
}
In the demo you say it out loud: “I don’t show internal errors to the outside because it is both security and contract stability.” This is one of the strongest “quality signals” you can show in 10 seconds.
How to demo HTTP live
You need 3–4 requests. For example:
- GET /health → 200 OK → text ok or JSON (whichever you chose).
- POST /api/v1/tasks → 201 Created → JSON with the task and an id.
- GET /api/v1/tasks/999 → 404 Not Found → error envelope.
- POST /api/v1/tasks with bad JSON/empty title → 400 Bad Request → error envelope + fields.
In a demo you do not have to “perfectly remember curl.” You can keep ready-made commands in the README. This is not cheating, it is taking care of yourself: you are showing the product, not your typing skills.
6. Request ID as a “production” signal
Request ID is minimal observability. Even if you do not have fancy tracing and metrics, a single request_id already lets you connect “an error on the client” with “a line in the logs.” And the nicest part: you can show it in 15 seconds, and it looks very convincing.
In a demo it is usually enough to say: “I support the X-Request-ID header. If it is provided — I use it. If it is not provided — I generate one. And I put it into logs with the key request_id.” You do not even have to show the generation (it can live inside middleware), but you should lock in the naming standards.
Mini example: constants for the header and logs
package api
const RequestIDHeader = "X-Request-ID"
const LogFieldRequestID = "request_id"
Why does this matter? Because without constants, in one place you will get X-Request-Id, in another X-REQUEST-ID, and in logs someone will write reqId. And that is the classic “death by a thousand cuts”: everything kind of works, but it is impossible to maintain.
7. Quality signals and demo prep
Quality signals are observable signs that do not require “taking your word for it.” In a demo you should not just have them, you should name them. Not “yeah there are tests,” but “here is the command, here is the result.” In Go this is especially natural: the tools are simple, and the discipline shows through details.
At the same time, it is important not to go into “show off.” A demo is not a contest of “who knows more go test flags.” It is better to show one or two strong signals.
A good 5–10 minute set looks like this:
- go test ./... passes (you can show it at the end as a single line).
- CLI/HTTP output is stable (no random ordering).
- Errors are contractual: in the CLI you have 0/1/2, in HTTP — statuses + envelope.
- Internal errors do not leak outside (5xx is always internal error).
- There is request_id as minimal observability.
If you want to add one more touch (only one!), show one unit test for parsing an id or for mapping errors to an exit code. It is quick and “to the point.”
Mini example: mapping an error to an exit code
package main
import "errors"
var ErrUsage = errors.New("usage")
func exitCode(err error) int {
if err == nil {
return 0
}
if errors.Is(err, ErrUsage) {
return 2
}
return 1
}
This is a good style: even if internally you return different errors, externally you map them predictably. By the way, the idea of “errors as values” is one of Go’s core mental models, and here it shows up very clearly.
Demo safety technique: how not to drown in the project
Before the finale, I want to say one “psychological” thing. When you are nervous, you start speeding up, and along with speeding up you start showing extra things: “and I also have an internal package,” “and here is middleware,” “and here is a diagram,” “and now I’ll open the adapters folder.” It looks like an attempt to hide a lack of results behind activity.
The calmest way to stay on track is to accept a rule in advance: in a demo we do not open the code editor. At all. Not even “for a second.” Because once you opened the code, you are no longer showing the product. You are showing the development process.
If you really want to show that “the code is neat,” you can do it indirectly: tests pass, the error format is stable, internals do not leak outside, request_id is consistent. That is no worse than browsing files, and it does not require belief.
8. Typical mistakes when demoing CLI/HTTP
Mistake #1: the demo turns into a story about architecture, not behavior.
This happens when you start with “here I have an internal folder, and here is domain…”. In the end, the listener still does not understand what the project does. The fix is simple: start with a CLI command or a curl request. First the result, then (if asked) the implementation.
Mistake #2: you do not lock in the stdout/stderr and exit-code contract, and everything looks random.
If you print some errors to stdout, some to stderr, and help returns code 1, the listener gets the impression that “it just runs.” It is much better to explicitly say the contract once: stdout is results, stderr is errors/usage; 0/2/1 as a basic contract.
Mistake #3: in HTTP, error formats “float,” because each handler writes its own way.
Today you have {"message":"bad"}, tomorrow {"error":"bad"}, the day after tomorrow http.Error. That does not make the client’s life easier. In the demo it is enough to show one error envelope and one writeError to make it clear: the format is stable, testable, maintainable.
Mistake #4: for 500-class errors you return err.Error() to the client.
It looks honest, but in practice it is almost always bad: you reveal internal details and make the contract unstable. In Go, errors are often wrapped and carry a lot of context; that context is useful for you in logs, but not for the client. In a demo it is better to show the opposite: for 5xx, the message is fixed, details are in logs.
Mistake #5: you show only the happy path, and it feels like “it works as long as you don’t touch it.”
One controlled negative scenario (CLI usage or an HTTP validation error) sharply increases trust. It is like a crash test: not because you love accidents, but because you thought about reality in advance.
GO TO FULL VERSION