Creating New Rules
Creating HTTP validation rules in Thymian is straightforward. You can generate a rule scaffold using the CLI or write one from scratch. This guide walks you through both approaches.
Quick Start with CLI
Section titled “Quick Start with CLI”The fastest way to create a new rule is using the interactive CLI generator:
thymian generate ruleThis command guides you through the rule creation process:
- Rule name — Unique identifier for your rule
- Severity —
error,warn, orhint - URL — Link to documentation (optional)
- Description — What the rule validates
- Rule types — One or more of:
static,analytics,test,informational - Applies to — Target participants:
client,server,proxy, etc.
The CLI generates a rule template that you can copy into your project:
import { httpRule } from '@thymian/core';
export default httpRule('your-rule-name').severity('error').type('static', 'analytics').description('Your rule description').appliesTo('server').done();Point the CLI at the generated .ts file and it runs — no build, no bundler, no Node
flags. thymian generate rule only ever emits a file that loads: .ts by default, .cjs
under --cjs, and never .mts/.cts. If --output names an extension that conflicts
with the mode (e.g. --cjs --output foo.js), the command declines rather than silently
rewriting your path.
Writing a Rule from Scratch
Section titled “Writing a Rule from Scratch”Step 1: Set Up Imports
Section titled “Step 1: Set Up Imports”Start by importing the necessary components:
import { httpRule, statusCode, not, responseHeader } from '@thymian/core';The @thymian/core package provides both the rule builder and filter expressions for matching HTTP transactions.
Step 2: Define Rule Metadata
Section titled “Step 2: Define Rule Metadata”Create the rule with its basic metadata:
export default httpRule('ensure-location-on-201').severity('error').type('static', 'analytics', 'test').description('201 Created responses must include a Location header').appliesTo('server');Best practices:
- Use descriptive names in kebab-case
- Choose appropriate severity based on requirement criticality
- Include clear descriptions that explain what is validated
Step 3: Add Validation Logic
Section titled “Step 3: Add Validation Logic”Add the validation logic using the common interface. Every validation call names two roles with separate keys:
.rule((ctx) => ctx.validateCommonHttpTransactions({ appliesTo: statusCode(201), violatedWhen: not(responseHeader('location')), }) )This rule:
- Applies to all transactions with status 201 (
appliesTo) - Reports a violation when the
Locationheader is missing (violatedWhen)
Applicability and Violation Condition
Section titled “Applicability and Violation Condition”appliesTois the rule’s Applicability: which transactions the rule speaks about, such as “responses with status 201”. It is required.violatedWhenis the rule’s Violation Condition: what is wrong with an applicable transaction, such as “noLocationheader”. It is required too. It is either a filter expression (the rule is violated when it matches) or a function that returns results. It runs only on transactions thatappliesToaccepts.
A transaction outside the rule’s applicability is never a violation. An object that leaves out either key does not compile.
Both keys are evaluated against whatever the command observes:
| Command | appliesTo is checked against |
violatedWhen is checked against |
|---|---|---|
lint |
the API specification | the same described transaction |
analyze |
the recorded request and response | the same recorded pair |
test |
the specification, to choose which requests to send, and then again against the response that actually came back | the response that came back |
In test, appliesTo therefore acts twice. A rule for 405 responses sends requests to operations the specification describes as answering 405, but stays silent when the server answers 204.
Step 4: Complete the Rule
Section titled “Step 4: Complete the Rule”Always end with .done():
.done();Complete Example
Section titled “Complete Example”Here’s a complete rule that enforces API versioning through custom headers:
import { httpRule } from '@thymian/core';import { constant, not, requestHeader } from '@thymian/core';
export default httpRule('require-api-version-header') .severity('error') .type('static', 'analytics', 'test') .url('https://api-guidelines.mycompany.com/versioning') .description('All API requests must include X-API-Version header') .appliesTo('client') .rule((ctx) => ctx.validateCommonHttpTransactions({ appliesTo: constant(true), violatedWhen: not(requestHeader('x-api-version')) })) .done();Validation Patterns
Section titled “Validation Patterns”There are three main patterns for writing the Violation Condition. appliesTo is an expression (a function in a lint-only rule, which works on the specification directly); what changes is the form of violatedWhen.
Pattern 1: Expression Condition
Section titled “Pattern 1: Expression Condition”Use a filter expression when a simple match says what is wrong:
.rule((ctx) => ctx.validateCommonHttpTransactions({ appliesTo: method('DELETE'), // Select DELETE requests violatedWhen: not(statusCodeRange(200, 204)), // Flag if status not 200-204 }))Every applicable transaction that matches violatedWhen produces one violation.
Pattern 2: Function Condition
Section titled “Pattern 2: Function Condition”Use a function for conditions an expression can’t express, such as parsing a header value, comparing dates or diffing two responses. The function runs only on transactions that appliesTo accepts and returns the violations it finds:
import { getHeader } from '@thymian/core';
.rule((ctx) => ctx.validateHttpTransactions({ appliesTo: statusCode(401), violatedWhen: (request, response, location) => { const authHeader = getHeader(response.headers, 'www-authenticate');
// A live-only fact, checked on the response that actually came back return typeof authHeader === 'undefined' || !isValidAuthHeader(authHeader) ? [{ location, violation: { message: 'Invalid WWW-Authenticate header' }, findings: [] }] : []; }, }))Pattern 3: Grouped Transactions
Section titled “Pattern 3: Grouped Transactions”Use the grouped variant when a violation depends on several transactions together, such as the same URL answered to GET and HEAD. It takes the same appliesTo and violatedWhen keys, plus a groupBy expression. violatedWhen is a function that receives each group, and only applicable transactions reach it:
.rule((ctx) => ctx.validateGroupedCommonHttpTransactions({ appliesTo: and(statusCode(200), or(method('GET'), method('HEAD'))), groupBy: url(), violatedWhen: (_group, transactions) => { const head = transactions.find(([req]) => equalsIgnoreCase(req.method, 'head')); const get = transactions.find(([req]) => equalsIgnoreCase(req.method, 'get'));
if (!head || !get) return [];
const [, headResponse, location] = head; const [, getResponse] = get;
return headResponse.headers.length < getResponse.headers.length ? [{ location, violation: { message: 'HEAD response is missing headers that GET carries' }, findings: [] }] : []; }, }))Common Filter Expressions
Section titled “Common Filter Expressions”Filter expressions from @thymian/core let you declaratively match HTTP transactions:
Request Filters
Section titled “Request Filters”method('GET'); // Match HTTP methodrequestHeader('content-type'); // Match header presencehasRequestBody(); // Has request bodyrequestMediaType('application/json'); // Match content typeauthorization(); // Has authorizationResponse Filters
Section titled “Response Filters”statusCode(404); // Match status codestatusCodeRange(400, 499); // Match status rangeresponseHeader('location'); // Match header presencehasResponseBody(); // Has response bodyresponseMediaType('application/json'); // Match content typeLogical Operators
Section titled “Logical Operators”and(method('POST'), statusCode(201)); // Both must matchor(statusCode(301), statusCode(302)); // Either can matchnot(responseHeader('location')); // Must NOT matchxor(...);Real-World Examples
Section titled “Real-World Examples”Example 1: Enforce Consistent Error Format
Section titled “Example 1: Enforce Consistent Error Format”Ensure all error responses use Problem Details format:
import { httpRule } from '@thymian/core';import { statusCodeRange, not, responseMediaType } from '@thymian/core';
export default httpRule('errors-use-problem-details') .severity('warn') .type('static', 'analytics') .description('Error responses should use application/problem+json format') .appliesTo('server') .rule((ctx) => ctx.validateCommonHttpTransactions({ appliesTo: statusCodeRange(400, 599), violatedWhen: not(responseMediaType('application/problem+json')) })) .done();Example 2: Enforce Correlation ID Tracking
Section titled “Example 2: Enforce Correlation ID Tracking”Ensure distributed tracing by requiring correlation IDs:
import { httpRule } from '@thymian/core';import { constant, not, requestHeader } from '@thymian/core';
export default httpRule('require-correlation-id') .severity('warn') .type('static', 'analytics') .description('Requests should include X-Correlation-ID for distributed tracing') .appliesTo('client') .rule((ctx) => ctx.validateCommonHttpTransactions({ appliesTo: constant(true), violatedWhen: not(requestHeader('x-correlation-id')) })) .done();Example 3: Require Deprecation Headers
Section titled “Example 3: Require Deprecation Headers”Ensure deprecated endpoints include proper sunset notices:
import { httpRule } from '@thymian/core';import { path, not, responseHeader } from '@thymian/core';
export default httpRule('deprecated-endpoints-require-sunset') .severity('error') .type('static', 'analytics') .description('Deprecated API endpoints must include Sunset header') .appliesTo('server') .rule((ctx) => ctx.validateCommonHttpTransactions({ appliesTo: path('/api/v1/*'), violatedWhen: not(responseHeader('sunset')) })) .done();Advanced: Custom Validation Functions
Section titled “Advanced: Custom Validation Functions”When an expression isn’t sufficient, make violatedWhen a function:
import { httpRule } from '@thymian/core';import { statusCode, getHeader } from '@thymian/core';
export default httpRule('validate-cache-control-directives') .severity('warn') .type('test', 'analytics') .description('Cache-Control header must include valid directives') .appliesTo('server') .rule((ctx) => ctx.validateHttpTransactions({ appliesTo: statusCode(200), // the specification can't declare Cache-Control, so it is checked below violatedWhen: (request, response, location) => { const cacheControl = getHeader(response.headers, 'cache-control');
if (typeof cacheControl === 'undefined') return [];
// Custom parsing and validation const directives = parseCacheControl(cacheControl);
// Return a result for every violation detected return hasValidDirectives(directives) ? [] : [{ location, violation: { message: 'Invalid Cache-Control directives' }, findings: [] }]; }, }), ) .done();
function parseCacheControl(header: string) { // Your parsing logic}
function hasValidDirectives(directives: any) { // Your validation logic}Common Pitfalls
Section titled “Common Pitfalls”1. Forgetting .done()
Section titled “1. Forgetting .done()”Always end your rule definition with .done():
// ❌ Missing .done()export default httpRule('my-rule') .severity('error') .type('static') .rule((ctx) => { ... });
// ✅ Correctexport default httpRule('my-rule') .severity('error') .type('static') .rule((ctx) => { ... }) .done(); // Don't forget!2. Using Wrong Filter Combination
Section titled “2. Using Wrong Filter Combination”Ensure your filter logic matches your intent:
// ❌ This will never match (GET is not POST)and(method('GET'), method('POST'));
// ✅ Use OR for alternativesor(method('GET'), method('POST'));Next Steps
Section titled “Next Steps”Now that you know how to create rules:
- Explore rule types in depth to understand context-specific features
- Learn about combining rule types for hybrid rules
- See how to use rules in your projects