Text Post API
Module 01 · foundations · beginner
Express vs n8n · Text Post API
The same "create post" API built twice: once as a plain Express server, once as an n8n workflow. Both follow the same contract and pass the same test script, so you can compare them line by line.
The contract
| Request | Response |
|---|---|
POST /posts {"author":"Ian","text":"Shipped my first n8n API"} |
201 {"id":"...","author":"Ian","text":"Shipped my first n8n API","length":24,"createdAt":"..."} |
POST /posts {"text":"First post from n8n"} |
201 with "author":"anonymous" |
POST /posts {}, {"text":""} or a text over 280 characters |
400 {"error":"text must be 1-280 characters"} |
Express version
server.js, about 15 lines:
const express = require('express');
const app = express();
let nextId = 1;
app.use(express.json());
app.post('/posts', (req, res) => {
const { author, text } = req.body ?? {};
if (!text || text.length > 280) return res.status(400).json({ error: 'text must be 1-280 characters' });
res.status(201).json({
id: String(nextId++),
author: author || 'anonymous',
text,
length: text.length,
createdAt: new Date().toISOString(),
});
});
app.listen(3000, () => console.log('http://localhost:3000/posts'));
package.json needs one dependency ("express": "^5.1.0") and a start script (node server.js). Run it with npm install && npm start.
n8n version
Six nodes, no server file:
Webhook (POST /posts) → Valid Post? ─true─→ Build Post → Respond Created (201)
└false─→ Build Error → Respond Error (400)
| Node | Settings |
|---|---|
| Webhook | POST, path posts, respond using a Respond to Webhook node |
| Valid Post? (IF, AND) | {{ $json.body.text }} is not empty, and {{ ($json.body.text || '').length }} ≤ 280 |
| Build Post (Edit Fields) | id = {{ $execution.id }}, author = {{ $json.body.author || 'anonymous' }}, text, length = {{ $json.body.text.length }}, createdAt = {{ $now.toISO() }} |
| Respond Created | Respond with JSON {{ $json }}, response code 201 |
| Build Error (Edit Fields) | error = text must be 1-280 characters |
| Respond Error | Respond with JSON {{ $json }}, response code 400 |
The full step-by-step build is in the lesson.
Line by line
| Job | Express | n8n |
|---|---|---|
| Start a server | app.listen(3000) |
n8n is already running |
Route POST /posts |
app.post('/posts', …) |
Webhook node, path posts |
| Parse the JSON body | app.use(express.json()) |
Automatic; the body lands in $json.body |
| Require a text | !text |
Valid Post? condition 1, is not empty |
| Check the length | text.length > 280 |
Valid Post? condition 2, ≤ 280 |
| Default the author | author || 'anonymous' |
Same expression in Build Post |
| Give the post an id | nextId++ counter |
$execution.id |
| Build the reply | Object literal in res.json(…) |
Build Post / Build Error Edit Fields nodes |
| Set the status code | res.status(201) / res.status(400) |
Response code on each Respond node |
| Send the reply | res.json(…) |
One Respond to Webhook node per branch |
Same test, both pass
./test/test.sh http://localhost:3000 # Express
./test/test.sh http://localhost:5678/webhook # n8n (published workflow)
Each run prints five PASS lines: valid post, default author, missing text, empty text, too long.
Gotchas each side hides
- Empty string:
!textrejects""for free. In n8n, the IF operator exists lets""through; you need is not empty. - Missing fields in expressions: Express's
!textcheck runs first, sotext.lengthis only read when there is a text. In n8n both IF conditions are evaluated, so the length check needs($json.body.text || '')or it fails on a missing text. - Every branch must respond: in Express, forgetting
res.jsonhangs the request, and so does a branch without a Respond node in n8n. In n8n it's easier to miss, because the canvas looks finished. - Status codes live on the Respond node: set
400on the wrong Respond node and your success path returns 400, or leave the default and a created post returns200instead of201. Content-Type: application/jsonis required in both: without it, Express leavesreq.bodyempty and n8n leaves$json.body.textundefined. Both then answer 400.
n8n vs custom code: pros and cons
n8n
Pros
- No server, routing or body parsing to write or deploy; the API exists as soon as the workflow is published.
- Visual: a client or teammate can read the flow on the canvas without reading code.
- Execution history shows every request's input and output, which makes debugging fast.
- 400+ integrations (Sheets, Gmail, CRMs, AI) are a node away, so saving the post to a sheet or posting it to Slack is one more node.
- Non-developers can change rules, like the character limit or the error message, without a deploy.
Cons
- Logic is spread across node settings, so code review, diffs and refactors are harder than in a file.
- Versioning means exporting workflow JSON; there is no natural Git workflow.
- Testing is end to end (like
test.sh); there are no unit tests for a single node. - Each request runs a workflow execution, which is slower and heavier than a plain route under high traffic.
- You depend on n8n's hosting, upgrades and node behavior; a version bump can change how a node works.
Custom code (Express)
Pros
- Full control over performance, error handling, middleware, auth and response shape.
- Lives in Git with code review, diffs, unit tests and CI.
- Runs anywhere Node runs, at very low cost per request.
- Easy to grow into a real application with a database, users and many routes.
Cons
- You write and maintain the plumbing: server, parsing, validation, logging, deployment and uptime.
- Every integration (Gmail, Sheets, CRMs) is an SDK to learn, configure and keep updated.
- Non-developers can't change it; every tweak is a code change and a deploy.
- Visibility into individual requests needs logging you set up yourself.
When to pick which
- Pick n8n when the API is glue between services, changes often, or will be maintained by someone who doesn't code.
- Pick custom code when the API is core product logic, needs high throughput or strict testing, or is going to grow into an application.
- Both is common: a custom app for the core product, with n8n handling the integrations and automations around it.