API smoke tests in five lines
After every deploy, someone curls a few endpoints to make sure the service is alive. Turn that ritual into a loop of http + assert steps you can run from your terminal or from CI.
blog/api-smoke-testsYou ship the service. Then you open a terminal and curl the health endpoint, eyeball the status code, maybe hit /version to confirm the right build went out. It takes thirty seconds and you do it every deploy. It's a real test — it just lives in your muscle memory instead of a file.
The problem with muscle-memory tests is that they don't run in CI, they don't fail loudly, and the new person on the team doesn't know they exist. The first sign that /orders is 500ing is a customer, not a curl.
OrchStep has http and assert as first-class step functions. So the post-deploy ritual becomes a short, real workflow: GET each endpoint, assert the status, and let a loop cover the whole list.
One endpoint, two steps
The core is a GET that captures the status, then an assert that checks it:
name: api-smoke-tests
defaults:
base: https://api.example.com
tasks:
smoke:
steps:
- name: get-health
func: http
args:
url: "{{ vars.base }}/healthz"
method: GET
- name: expect-200
func: assert
args:
condition: '{{ eq steps.get-health.status_code 200 }}'
message: "/healthz did not return 200"That's the whole idea: http makes the request and exposes result.status_code (reachable from later steps as steps.get-health.status_code), and assert turns the expectation into a step that fails the run when it isn't met. No framework, no test harness — the same binary you use for everything else.
Cover the whole list with a loop
You rarely check one endpoint. Define the list once and loop:
name: api-smoke-tests
# Override at the CLI: --var base=https://staging.api.example.com
defaults:
base: https://api.example.com
endpoints:
- /healthz
- /version
- /users
- /orders
tasks:
smoke:
steps:
- name: check
func: shell
do: echo "GET {{ vars.base }}{{ loop.item }} -> expect 200"
loop:
items: '{{ vars.endpoints }}'The runnable demo echos each request so it works offline and in any environment. To make it hit the network, swap the shell step for the http + assert pair below — same loop, real requests:
- name: check
func: http
args:
url: "{{ vars.base }}{{ loop.item }}"
method: GET
loop:
items: '{{ vars.endpoints }}'
- name: assert-ok
func: assert
args:
condition: '{{ lt steps.check.status_code 400 }}'
message: "an endpoint returned a 4xx/5xx"Run it
orchstep run smoke --var base=https://staging.api.example.comPoint it at staging, prod, or a preview environment with one --var. The run exits non-zero the moment an assert fails, which is exactly what you want from a smoke test.
Run it from CI
Because the binary refuses to hang waiting for input, the same task is safe to drop into a pipeline step right after deploy:
orchstep run smoke --var base=$DEPLOY_URL --output json--output json gives your CI a machine-readable result; the exit code gates the pipeline. The thirty-second ritual is now a gate that runs on every deploy, for everyone.
What you actually gained
| Concern | curl by hand | OrchStep |
|---|---|---|
| Status check | eyeball the output | assert step, fails the run |
| Many endpoints | several curl lines | one loop over a list |
| Target env | edit the command | --var base=... |
| Runs in CI | no | yes, exit code + --output json |
| New teammate finds it | asks in Slack | it's a task in the repo |
If a one-line curl \|\| exit 1 already covers you, keep it. The moment you have more than one endpoint or want it in CI, the loop pays for itself.
Where to go next
- The http function — methods, headers, body, and
result.status_code - Quick Start — your first workflow in two minutes
- Variables & Outputs — capturing and reusing step outputs
Got a post-deploy curl ritual? Put the endpoints in a list and let the loop run it for everyone.
curl -fsSL https://orchstep.dev/install.sh | sh