Web Pipe is a domain-specific language (DSL) and runtime for building HTTP APIs through composable middleware pipelines. Instead of writing imperative code, you declare routes and data transformations that flow through a series of steps—from request to response.
Web Pipe lets you build APIs by composing middleware steps into pipelines. Each step receives JSON, transforms it, and passes the result to the next step. The final step produces the HTTP response.
Core Philosophy:
- Declarative over Imperative: Describe what you want, not how to do it
- Composition over Complexity: Build complex behavior from simple, reusable pieces
- Data Flow over Control Flow: Transform JSON through a series of steps
app.get('/users/:id', async (req, res) => {
const userId = parseInt(req.params.id);
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
const orders = await db.query('SELECT * FROM orders WHERE user_id = $1', [userId]);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
user: user.rows[0],
orders: orders.rows,
orderCount: orders.rowCount
});
});GET /users/:id
|> pg([.params.id]): `SELECT * FROM users WHERE id = $1` @result(user)
|> pg([.params.id]): `SELECT * FROM orders WHERE user_id = $1` @result(orders)
|> jq: `{
user: .data.user.rows[0],
orders: .data.orders.rows,
orderCount: .data.orders.rowCount
}`
Benefits:
- No boilerplate (no imports, app setup, route handler functions)
- Automatic error handling (validation, DB errors, network failures)
- Built-in result accumulation with
@resulttags - Type-safe SQL parameter binding
- Declarative data flow
Map HTTP requests to processing pipelines:
GET /users/:id
POST /users
PUT /users/:id
DELETE /users/:id
Reusable sequences of middleware steps:
pipeline authenticateUser =
|> auth: "required"
|> jq: `{ userId: .user.id }`
Built-in transformations and operations:
- jq: Transform JSON
- pg: Query PostgreSQL
- fetch: Call external APIs
- graphql: Execute GraphQL queries
- handlebars: Render HTML templates
- lua: Custom scripting
- auth: Authentication
- cache: Response caching
- validate: Input validation
- log: Request logging
Reusable SQL queries, templates, and configuration:
pg getUserById = `SELECT * FROM users WHERE id = $1`
handlebars userCard = `<div>{{name}}</div>`
Environment-specific settings:
config pg {
host: $WP_PG_HOST || "localhost"
database: "myapp"
}
config auth {
sessionTtl: 604800
}
Each middleware step:
- Receives the current JSON state
- Performs an operation (query, transform, HTTP call)
- Returns updated JSON state or an error
- Passes the result to the next step
GET /users/:id
# State: { params: { id: "123" }, query: {}, body: {}, headers: {} }
|> jq: `{ userId: (.params.id | tonumber) }`
# State: { userId: 123 }
|> pg([.userId]): `SELECT * FROM users WHERE id = $1`
# State: { userId: 123, data: { rows: [...], rowCount: 1 } }
|> jq: `{ user: .data.rows[0] }`
# Response: { user: { id: 123, name: "Alice", ... } }
Accumulate multiple query results:
GET /dashboard
|> pg: `SELECT * FROM users` @result(users)
|> pg: `SELECT * FROM orders` @result(orders)
|> pg: `SELECT * FROM products` @result(products)
|> jq: `{
userCount: .data.users.rowCount,
orderCount: .data.orders.rowCount,
productCount: .data.products.rowCount
}`
Execute independent tasks concurrently:
GET /dashboard
|> fetch: `https://api.example.com/users` @async(users)
|> fetch: `https://api.example.com/stats` @async(stats)
|> pg: `SELECT * FROM local_data` @async(local)
|> join: `users, stats, local`
|> jq: `{
external: .async.users.data,
statistics: .async.stats.data,
internal: .async.local.data.rows
}`
Branch logic with dispatch, @when, @guard:
GET /content
|> dispatch
case @env(production):
|> pg: `SELECT * FROM prod_content`
case @env(development):
|> pg: `SELECT * FROM dev_content`
default:
|> jq: `{ error: "Unknown environment" }`
Native GraphQL schema and resolver support:
graphql {
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Query {
user(id: ID!): User
users: [User!]!
}
}
resolver Query.users =
|> pg: `SELECT * FROM users`
|> jq: `{ users: .data.rows }`
resolver User.posts =
|> loader(.parent.id): PostsByUserLoader
BDD-style testing with mocking:
describe "User API"
let userId = 1
with mock pg.getUserById returning `{
id: $userId,
name: "Alice",
email: "alice@example.com"
}`
it "fetches user by ID"
when calling GET /users/{{userId}}
then status is 200
and response.user.name equals "Alice"
Here's a complete example showing data flow through a pipeline:
config pg {
host: $WP_PG_HOST || "localhost"
database: "myapp"
}
pipeline loadUser =
|> pg([.userId]): `SELECT * FROM users WHERE id = $1` @result(user)
GET /users/:id/profile
# 1. Extract user ID from URL
|> jq: `{ userId: (.params.id | tonumber) }`
# 2. Load user data using pipeline
|> pipeline: loadUser
# 3. Check if user exists
|> if
|> jq: `.data.user.rows | length == 0`
then:
|> jq: `{ error: "User not found", status: 404 }`
end
# 4. Load related orders in parallel
|> pg([.userId]): `SELECT * FROM orders WHERE user_id = $1` @result(orders)
# 5. Transform and return response
|> jq: `{
user: .data.user.rows[0],
orders: .data.orders.rows,
totalOrders: .data.orders.rowCount
}`
Great for:
- REST APIs with database backends
- GraphQL APIs
- API gateways and proxies
- Data aggregation services
- Backend-for-frontend (BFF) layers
- Microservices with simple logic
Not ideal for:
- Complex business logic with heavy computation
- Real-time websocket servers (though HTTP SSE works)
- Applications requiring custom protocol handling
- Projects where team prefers traditional programming languages
┌─────────────────────────────────────────────────┐
│ HTTP Request │
│ GET /users/123?include=orders │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Request Parser │
│ { params: {id: "123"}, query: {include: ...} } │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ featureFlags Pipeline │
│ (runs before every route if defined) │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Route Pipeline │
│ |> jq: transform │
│ |> pg: query database │
│ |> fetch: external API │
│ |> jq: format response │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Response Builder │
│ Status: 200, Content-Type: application/json │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ HTTP Response │
│ { user: {...}, orders: [...] } │
└─────────────────────────────────────────────────┘
- Getting Started: Install and run your first Web Pipe application
- DSL Syntax: Complete language reference
- Routes & Pipelines: Learn about composable pipelines
- GraphQL: Build GraphQL APIs with Web Pipe
Web Pipe is inspired by functional programming and Unix pipes. Like Unix commands that transform text streams, Web Pipe middleware transforms JSON through a pipeline. The goal is to make API development:
- Simple: Express complex logic through composition
- Declarative: Describe transformations, not control flow
- Testable: Built-in mocking and BDD testing
- Fast: Async execution and optimized runtime
- Maintainable: Small, focused middleware steps
Web Pipe is not trying to replace general-purpose programming languages—it's a specialized tool for a specific domain: building HTTP APIs that transform and aggregate data.