- name
- create-tutorial
- description
- Scaffold a new Membrane API Gateway tutorial in the api-gateway repo — the numbered self-teaching YAML under distribution/tutorials/<category>/, its support files and README links, and the matching auto-discovered integration test. Use whenever the user asks to create, add, write, or scaffold a tutorial (or a tutorial step), even if they only describe the gateway behavior they want to teach and don't say the word "skill" or name the files.
# Create a Tutorial
In this repo a tutorial is usually a **runnable, self-teaching YAML config**.
The leading comment block of the file is the lesson; the config below it is the working
example the reader runs. A complete tutorial therefore has three parts, and they must stay
in sync:
1. **The numbered YAML** in `distribution/tutorials/<category>/` — the lesson plus the config.
2. **Category support files** — `membrane.sh`, `membrane.cmd`, a `README.md` (and
`run-docker.*` where the category offers Docker), so the reader can actually start it.
3. **An integration test** under `distribution/src/test/java/com/predic8/membrane/tutorials/…`
so the tutorial can't silently rot. These tests unzip the built distribution and start
Membrane against the tutorial's YAML, so a broken example fails CI.
A tutorial step can also be a **plain `.md` file** — use this when the step needs no local
Membrane config (e.g. a curl-only walkthrough against a hosted API). A `.md` step gets no
integration test; it just lives alongside the numbered YAMLs and is linked from the README.
Work through the steps below in order. The reader of a tutorial follows the file top-to-bottom
with no other docs open, so the single most important thing is that the YAML teaches itself.
## Step 1 — Choose the category and filename
Categories live under `distribution/tutorials/`: `getting-started`, `json`, `xml`, `advanced`,
`transformation`, `security`, `jwt`, `api-keys`, `oauth2`, `soap`, `soap-rest-converter`,
`wsdl-to-openapi`, `orchestration`, `sse`, `mcp`, `llm-gateway`. Pick the one that fits; only
create a new category if nothing fits (see "New category" at the end).
Files are numbered `NN-Title.yaml` (e.g. `10-PathParameters.yaml`). The number sets reading
order. List the directory and pick the next gap — convention leaves room between numbers
(10, 20, 30…) so steps can be inserted later. Match the existing `PascalCase`-with-hyphens
title style.
## Step 2 — Write the YAML (the lesson lives here)
Open a sibling file in the same category and mirror its shape. Every tutorial YAML has the
same skeleton:
```yaml
# yaml-language-server: $schema=https://www.membrane-api.io/v7.2.4.json
#
# Tutorial: <Human Readable Name>
#
# <One sentence on what this teaches.>
#
# 1.) Start Membrane:
# ./membrane.sh -c NN-Title.yaml
#
# 2.) Call the API:
# curl localhost:2000/...
#
# <Expected output — paste it verbatim.>
#
# 3.) Continue with file NN-Next.yaml # only in sequential tutorials, see below
api:
port: 2000
...
```
Hold to these conventions — they're what makes the set feel like one coherent course:
- **Pin the schema line** to whatever sibling files use (grep one: `grep -m1 schema=
distribution/tutorials/<category>/*.yaml`). It tracks the current release version, so don't
invent a number.
- **Keep the comment block short.** Steps are a command and its expected output — nothing more.
Don't explain what the config does; the reader can see the config. Don't repeat information
the `name:` field already provides.
- **No section-header comments** above each `api:` block. The `name:` field describes the API.
- **No trailing `# ...` after a command** in a step. Readers copy the line into their shell,
and Windows `cmd` doesn't treat `#` as a comment, so it would pass the text to the command.
Put prompts and notes on their own line:
```
# curl -u alice localhost:2000
# enter password: secret
```
Show every prompt an interactive tool asks, in order (e.g. `htpasswd` also asks
`Re-type new password:`), so the reader isn't surprised by one the step left out.
- **`NOTE:` blocks go at the end of the file**, after the config. Caveats, alternatives and
background (unsupported hash formats, other databases, "plaintext only for demos") would
otherwise come between the steps and the config. The top block stays steps only.
- **Inline comments only for the non-obvious WHY** — a surprising built-in, a hidden constraint.
Never comment what the interceptor name already says.
- **Keep the config minimal and focused** on the one concept the step teaches. Tutorials favor
clarity over completeness; richer setups belong under `distribution/examples/`.
- **Port 2000** is the house convention for tutorial listeners unless the lesson needs more.
- Use `return: status: 200` not `return: {}`.
- **"Continue with file NN-Title.yaml" only where the reader goes through the files in order:**
`getting-started/`, `advanced/`, and numbered sub-step series within a topic (e.g. 20, 21,
22, 23). Every other category (routing, security, openapi, ...) is random access, so its
files get no "Continue with" pointer. When adding to a sequential set, end the previous
file's comment block with a pointer to the new one.
## Step 3 — Make the category runnable
The reader runs `./membrane.sh -c <file>` from the category directory, so that directory needs
the launcher scripts and a README.
- If `membrane.sh` / `membrane.cmd` are missing from the category dir, copy them verbatim from
a sibling category — they are identical generic launchers that walk up to find `MEMBRANE_HOME`.
- Copy `run-docker.sh` / `run-docker.cmd` only if the category documents a Docker workflow.
- **`README.md`**: the category README points at the first file in the chain ("open
[NN-First.yaml](NN-First.yaml) and follow the instructions there"). If you added the first
file of a new category, write this README modeled on a sibling's.
## Step 4 — Write the integration test (don't skip this)
Without a test the example will drift out of sync with the code and break unnoticed — that's the
whole reason these tests exist. The harness starts Membrane from the **built distribution** with
your YAML and lets you assert against the live gateway.
Test package mirrors the category but with **underscores**: dir `getting-started` →
package `com.predic8.membrane.tutorials.getting_started`. The `ExampleTests` `@Suite` selects the
whole `com.predic8.membrane.tutorials` package, so a test placed correctly is **auto-discovered
— no registration needed.**
Each category has an `Abstract<Category>TutorialTest` that fixes `getTutorialDir()` (this is where
the hyphen dir name is restored). Your concrete test extends it, names the YAML, and asserts:
```java
/* <copy the Apache 2.0 license header verbatim from a sibling test — keep its year> */
package com.predic8.membrane.tutorials.<category_with_underscores>;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.containsString;
public class <Name>TutorialTest extends Abstract<Category>TutorialTest {
@Override
protected String getTutorialYaml() {
return "NN-Title.yaml";
}
@Test
void <whatItVerifies>() {
// @formatter:off
given()
.when()
.get("http://localhost:2000/...")
.then()
.statusCode(200)
.body(containsString("..."));
// @formatter:on
}
}
```
Notes:
- Copy the license header from an existing sibling test rather than retyping it, so the year and
wording match the repo.
- Assert the **observable behavior the YAML comments promise** in Step 2 — if the lesson says
"you should see Customer: 7", assert exactly that. The test is the executable form of the lesson.
- If you created a brand-new category, also create its `Abstract<Category>TutorialTest` modeled on
an existing one — it just returns the hyphenated dir name from `getTutorialDir()`.
- **Sub-categories** (e.g. `llm-gateway/google`) need their own abstract base returning the full
relative path (`llm-gateway/google`), not just the top-level category abstract. Model on
`AbstractAiTutorialTest` / `AbstractGoogleTutorialTest` in
`distribution/src/test/java/com/predic8/membrane/tutorials/`.
- **`.md` steps** have no integration test — skip this step for them.
## Step 5 — Verify
Run the test through the existing `run-example-test` skill. Pass `-b` so the distribution is
rebuilt first: the tutorial YAML ships **inside** `distribution/target/*.zip`, and the tests run
from the unzipped distribution, so an un-rebuilt run would test the old YAML (or none).
```bash
.claude/skills/run-example-test/run-example-test.sh -b <Name>TutorialTest
```
A green run means the example actually starts and behaves as the lesson claims.
## Common pitfalls
### Template JSON auto-quoting
When a `template` interceptor has `contentType: application/json`, the engine **automatically
adds quotes** around every `${...}` interpolation. Writing `"${expr}"` produces `""value""` and
breaks JSON. Always write interpolations **unquoted**:
```yaml
- template:
contentType: application/json
src: |
{
"sub": ${user()},
"aud": "demo-resource"
}
```
String literals that are not interpolated keep their quotes as normal.
### Built-in functions in Groovy templates
Prefer the built-ins over raw property access:
- `user()` — authenticated username (from `basicAuthentication` security scheme)
- `scopes()` — list of granted scopes
- `hasScope("read")` — boolean scope check
Avoid the verbose `${property.'membrane.security.schemes'[0].username}` — use `${user()}` instead.
## New category (only when nothing fits)
1. `distribution/tutorials/<new-category>/` with the first `NN-*.yaml`, `membrane.sh`,
`membrane.cmd`, and a `README.md`.
2. Add a section linking the new category in `distribution/tutorials/README.md`.
3. Create `com.predic8.membrane.tutorials.<new_category>.Abstract<NewCategory>TutorialTest`
extending `AbstractMembraneTutorialTest`, returning the hyphenated dir from `getTutorialDir()`.
4. Add concrete tests as in Step 4.
## Checklist
- [ ] `NN-Title.yaml` (or `.md` for curl-only steps) created with schema line, short numbered comment block, minimal config
- [ ] Comment block: steps = command + expected output only; no prose repeating the config
- [ ] No trailing `# ...` on command lines (Windows `cmd` passes it to the command); prompts on their own line
- [ ] `NOTE:` blocks placed after the config at the end of the file, not in the top step block
- [ ] No section-header comments above `api:` blocks; `name:` describes them
- [ ] `return: status: 200` used (not `return: {}`)
- [ ] Template interpolations unquoted when `contentType: application/json`
- [ ] Previous file points to it ("Continue with…") only in a sequential set (getting-started, advanced, NN/NN+1 sub-step series); none in random-access categories
- [ ] Category has `membrane.sh`, `membrane.cmd`, `README.md` (and `run-docker.*` if used)
- [ ] `<Name>TutorialTest` in the underscore package, extends the category abstract base, has the license header
- [ ] Sub-category has its own abstract base returning the full relative path
- [ ] `.md` steps have no integration test
- [ ] Assertions match the behavior the YAML comments promise
- [ ] `run-example-test.sh -b <Name>TutorialTest` is green
在 GitHub 查看