Routing & Request Validation
When someone calls your API, Fluxify automatically figures out which workflow to run, checks the request data against your defined rules, and only then starts executing your logic. This page explains how that works and what your callers can expect to receive.
How Requests Are Matched
Every route you create in Fluxify has an HTTP method (like GET or POST) and a URL path (like /users/:id). When a request comes in, Fluxify matches it to the right workflow instantly.
- If a match is found, the workflow runs.
- If nothing matches, the caller gets a
404response.
Path parameters like :id or :slug are captured automatically and made available inside your workflow.
Hot Reloading
Routes update in real time. When you create or change a route in the dashboard, the change takes effect immediately — no server restart needed.
Experimental CPU-Stall Timeout
Every route has a timeoutSeconds setting. It defaults to 30 seconds and can only be increased. The setting is carried with the compiled route whenever you save it, so a timeout change takes effect through the normal route hot reload path.
The timeout becomes active only when the project's experimental worker-timeout policy is enabled:
curl -X PUT "http://localhost:8080/_/admin/api/v1/projects/<project-id>/settings/keys" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access-token>" \
-d '{"key":"experimental.workerTimeouts.enabled","value":"true"}'This policy detects synchronous CPU stalls, such as an infinite loop in custom JavaScript. It does not time out a long asynchronous operation while the execution process remains responsive. When a CPU stall exceeds the configured route timeout, the in-flight connection is dropped and only the isolated execution process is replaced; the worker container continues receiving NATS hot updates.
When the policy is disabled (the default), no execution heartbeats or timeout tracking are created.
What Happens Before Your Workflow Runs
Before Fluxify executes a single block in your workflow, it runs through a quick validation pipeline:
- Match the route — find the right workflow for this request.
- Check the content type — make sure the caller sent a format this route accepts.
- Parse the request body — turn it into data your blocks can use.
- Validate the body — check it against your Body Schema (if set).
- Validate query parameters — check URL query strings against your Query Schema (if set).
- Validate path parameters — check URL path values against your Params Schema (if set).
- Run your workflow — only if everything above passed. If the route has middlewares, the ones set to run before it go first, and the ones set to run after it get the reply.
If any validation step fails, the workflow never runs and the caller immediately gets a clear error response.
Accepted Content Types
A route declares which body formats it accepts. New routes accept application/json only; choose more under Accepted content types when you create the route, or later in the route's Settings.
| Content type | What your workflow receives |
|---|---|
application/json | The parsed JSON value |
application/x-www-form-urlencoded | An object of field names and text values |
multipart/form-data | An object of text fields plus any uploaded files |
application/octet-stream | The raw binary body as a Blob, or a ReadableStream<Uint8Array> when the route uses Pipe request body as a stream |
text/plain | The text as sent |
For application/octet-stream, turn on Pipe request body as a stream under the body settings to hand getRequestBody() the live ReadableStream<Uint8Array> instead of collecting the bytes into a Blob. Nothing is buffered or size-limited by Fluxify in that mode, so pipe it straight to a file, S3, or another destination. Stream routes accept only application/octet-stream, have no body schema, and cannot be called with x-fluxify-reply: async.
A request in any other format — or in a format this particular route wasn't set up for — is rejected with 415 and the workflow never runs. GET and DELETE requests carry no body at all.
Bodies have a size limit
8 MB by default, and larger requests get a 413. See request body size.
Request Validation
Each route optionally has three schemas you can configure in the Schema Editor:
| Schema | What It Validates |
|---|---|
| Body Schema | The request body (POST, PUT): JSON, form fields, uploaded files or raw binary |
| Query Schema | URL query parameters (e.g. ?page=2&limit=10) |
| Params Schema | URL path values (e.g. the 42 in /users/42) |
You only need to configure the ones that matter for your route — unused schemas are simply skipped.
Available Data Types
| Type | What It Accepts |
|---|---|
String | Any text, with optional rules like minimum length, maximum length, or a required format |
Integer | A whole number, with optional min/max |
Float | A decimal number, with optional min/max |
Boolean | true or false |
Array | A list of items, with optional min/max item count |
Object | A structured set of named fields (can be nested) |
Enum | One specific value from a predefined list |
File | One uploaded file from a multipart/form-data request, with optional size and file-type rules |
Blob | A raw binary body (application/octet-stream), with optional size rules |
Use JavaScript | Custom validation logic you write yourself |
A String field can also pick a ready-made Format. Any other value is rejected with a clear message, like Must be a valid UUID v4:
| Format | Accepts |
|---|---|
| UUID v4 / UUID v7 | A UUID of that version, e.g. 550e8400-e29b-41d4-a716-446655440000 |
| An email address | |
| URL | A URL with any protocol and a host, e.g. https://a.com, tcp://host:9000, rediss://host/0 |
| IPv4 / IPv6 address | An IP address of that version |
| ISO 8601 date-time | e.g. 2024-01-01T10:00:00Z or 2024-01-01T10:00:00+05:30 |
TIP
Need something these don't cover, like only https URLs? Use a regex pattern or Use JavaScript.
Form Fields Are Converted for You
Everything in a form body (application/x-www-form-urlencoded or multipart/form-data) arrives as text, because that is how forms are sent. Fluxify converts form fields to the type your schema declares before checking them, the same way it does for query and path values:
| Field type | Sent in the form | Your workflow receives |
|---|---|---|
Integer | age=25 | 25 |
Boolean | active=false | false |
A JSON body is not converted. JSON has real numbers and booleans, so { "age": "25" } fails an Integer field. Send { "age": 25 } instead.
Accepting Several Files
A File field takes exactly one file. To let callers upload more than one under the same name, make the field an Array and set its item type to File:
| Field set up as | One file sent | Several files sent |
|---|---|---|
File | Accepted | Rejected with 400 |
Array of File | Accepted, as a list of one | Accepted, as a list |
Size and file-type rules on the item apply to each file. Minimum and maximum item counts on the array limit how many files can be sent.
Sending several files
In a multipart request, repeat the same field name once per file. With curl: curl -F [email protected] -F [email protected] ...
Marking Fields as Required
Each field in your schema can be marked as required or optional. Required fields must be present — if they're missing, the request is rejected with an error.
Custom JavaScript Validation
For rules that can't be expressed with the built-in types, you can switch a field to Use JavaScript and write your own logic. Your code receives the field's value and should either return true (validation passes) or throw a ValidationError with a custom message.
// Example: reject discount codes that don't start with "SALE"
if (!input.startsWith("SALE")) {
throw new ValidationError({
code: "INVALID_CODE",
message: "Discount codes must start with SALE"
});
}
return true;Whatever you pass to ValidationError is returned directly to the caller — giving you full control over your error messages.
Validation schemas and their trusted JavaScript validators are compiled when a route artifact loads or hot-reloads. Requests only call the cached validator; they never compile schema definitions or request-provided js: strings. Values received from a caller are always treated as data, including strings beginning with js:.
Validation Error Response
When a request fails validation, the caller receives a 400 Bad Request with a JSON body that clearly describes what went wrong.
{
"message": "Body validation failed",
"errors": [
{
"path": "address.zipCode",
"property": "zipCode",
"errors": ["String must contain at least 5 character(s)"]
},
{
"path": "age",
"property": "age",
"errors": ["Expected number, received string"]
}
]
}| Field | Description |
|---|---|
message | Which part of the request failed: body, query, or path parameters |
errors[].path | The full path to the failing field using dot notation (e.g. address.zipCode) |
errors[].property | Just the field name that failed (e.g. zipCode) |
errors[].errors | One or more error messages for that field |
When a custom JavaScript validator throws a ValidationError, its payload appears verbatim in errors[].errors — exactly as you wrote it.
Error Responses at a Glance
Here's every possible outcome and what the caller receives:
| Situation | Status | Response |
|---|---|---|
| Route doesn't exist | 404 | { "message": "Route not found" } |
| Content type not accepted | 415 | { "message": "Unsupported content type ..." } |
| Body too large | 413 | { "message": "Request body exceeds the ...KB limit" } |
| Body couldn't be read | 400 | { "message": "Malformed ... body: ..." } |
| Validation failed | 400 | { "message": "...", "errors": [...] } |
| Workflow error | 500 | { "error": "description" } |
| Unexpected crash | 500 | { "message": "Internal server error" } |
| Success | 200 | Whatever your Response block returns |
Custom Status Codes
Your workflow can return any HTTP status code you choose using the Response block — the defaults above only apply when no status is explicitly set.
Turning a Route On or Off
A route has an Enabled switch for each environment, in its settings under Advanced. A new route is on in Development and off in Production. Callers of your public URL get 404 until you turn Production on. A route that is off in an environment returns 404 there.
Trying a Route in the Playground
The Playground sends a real request to your route and shows the response. Open it with the Playground button on the Routes page, or the play button on the route's canvas.
A Run in Production toggle sits at the top. It opens on Development and does not remember your choice, so the next time you open the playground you are back on Development.
| Toggle | Where the request goes | What it runs with |
|---|---|---|
| Off (default, Development) | The route's development address, /_/dev + the path | Development worker, development values |
| On (Production) | The route's public address, exactly as a caller uses it | Production's promoted version, production values |
Development asks for nothing extra: the route must be on in Development, which a new route is, and Fluxify adds the development token for you. If no development worker is running, the playground says so and cannot send.
Production is offered only once you have promoted a version. Until then the toggle is disabled and says Not promoted yet. The first time you switch it on, the playground asks you to confirm: "This calls production: real data, real side effects." A production call really reaches your live route and can change real data.
When production recording is on for the route, a production call shows an Open recording link once the run is recorded, so you can open it on the Executions page. When recording is off, there is nothing to open and the link does not appear.
For POST and PUT routes, pick a content type on the Body tab. The editor changes to match it:
| Content type | How you fill in the body |
|---|---|
application/json, text/plain | Type it into the editor |
| Form types | Fill in the fields from your body schema. With no schema, add fields yourself; in multipart/form-data each field can hold text or a file |
application/octet-stream | Choose a file, or paste the data as base64 |
You can send a body even when the route has no body schema. Leave Validate on to have the playground check your input against the route's schemas before sending; turn it off to see how the route itself answers bad input.
Next Steps
- 🧩 Explore the Blocks Reference — build the workflow logic that runs after validation
- 🛡️ Middlewares — share checks like API keys or logging across many routes
- ✍️ Scripting Guide — learn more about writing custom JavaScript
- ⚙️ App Config — manage secrets your validators can reference
