Module 01 · foundations · beginner
Lesson 01 · Text Post API
Goal
Build a tweet-style "create post" API in n8n: it accepts a text, checks it is 1–280 characters, and responds with the created post or an error. Almost every real automation starts with a trigger like this, whether it's a form submission, a Stripe event or a frontend calling your workflow.
Prerequisites
- n8n running locally (
playground/,docker compose up -d) curlin your terminal
Concepts
- Items: every node receives and emits a list of items shaped like
[{ json: {...} }]. Click any node after a run to see its input and output. - Expressions:
{{ ... }}is JavaScript evaluated per item.$jsonis the current item; the webhook payload lives under$json.body. Plain JavaScript works inside, so{{ $json.body.text.length }}counts characters and{{ $json.body.author || 'anonymous' }}sets a default. - Test vs production URL:
/webhook-test/postsworks once each time you click Listen for test event./webhook/postsworks only when the workflow is published. - Respond to Webhook: with the Webhook's response mode set to Using Respond to Webhook Node, you decide exactly what the caller gets back, including the status code.
- Status codes:
201 Createdmeans "I made the thing you asked for".400 Bad Requestmeans "your input is wrong, fix it and try again".
Build it
Step 1: Webhook trigger
Add a Webhook node. Set:
- HTTP Method:
POST - Path:
posts - Respond:
Using 'Respond to Webhook' Node
Step 2: Validate with IF
Add an IF node after the Webhook and rename it Valid Post?. Add two conditions and keep the combinator on AND:
- Value
{{ $json.body.text }}, operator String → is not empty - Value
{{ ($json.body.text || '').length }}, operator Number → is less than or equal to, value280
Use is not empty, not exists: {"text": ""} exists but is empty. The || '' in the second condition stops the expression failing when text is missing.
Step 3: Success branch
From the true output, add Edit Fields (Set) named Build Post with:
id(String) ={{ $execution.id }}author(String) ={{ $json.body.author || 'anonymous' }}text(String) ={{ $json.body.text }}length(Number) ={{ $json.body.text.length }}createdAt(String) ={{ $now.toISO() }}
Then a Respond to Webhook named Respond Created: Respond With JSON, body {{ $json }}, Options → Response Code = 201.
Step 4: Error branch
From the false output, add Edit Fields (Set) named Build Error with:
error(String) =text must be 1-280 characters(no quotes: n8n already knows it's a string)
Then a Respond to Webhook named Respond Error: Respond With JSON, body {{ $json }}, Options → Response Code = 400.
Step 5: Save and publish
Save (Cmd/Ctrl+S), then click Publish.
Test it
./test/test.sh
Expected:
PASS valid post (201) {"id":"...","author":"Ian","text":"Shipped my first n8n API","length":24,"createdAt":"..."}
PASS default author (201) {"id":"...","author":"anonymous","text":"First post from n8n","length":19,"createdAt":"..."}
PASS missing text (400) {"error":"text must be 1-280 characters"}
PASS empty text (400) {"error":"text must be 1-280 characters"}
PASS too long (400) {"error":"text must be 1-280 characters"}
Common problems
- 404 "webhook not registered": test URL without clicking Listen, or production URL without publishing.
- Every post is rejected: you forgot
-H 'Content-Type: application/json', so the body isn't parsed as JSON. - Request hangs: a branch ends without a Respond to Webhook node.
- Success returns 400: you duplicated the error Respond node for the success branch and kept its response code. Each Respond node has its own status code.
- Empty posts get through: the first condition uses exists instead of is not empty.
- IF errors on missing text: the length condition is
{{ $json.body.text.length }}without|| '', which fails when there is no text. - Error text has extra quotes: you typed
"text must be 1-280 characters"with quotes into the String field.