Pre-Request Scripts and Dynamic Requests: Collection Scripts vs Declarative Spec
Quick answer: Pre-request scripts solve a real problem, computing a dynamic value like a signature or a timestamp at send time, but they bury request behavior in imperative code that only runs inside one collection tool and is hard to test or reuse. A declarative spec expresses the same requirement as data, where the value comes from, which header it maps to, and how the signature is built, so the behavior is portable, reviewable, and generated consistently across clients. The goal is not to eliminate dynamic requests; it is to move them from hidden scripts into a contract that every tool and the CI pipeline can understand.
Pre-request scripts are where a lot of collection logic accumulates. A developer writes a few lines to hash a body, set an auth header, or read a token from a previous response, and it works. Over time the scripts grow into an undocumented program: order-dependent, duplicated across collections, and invisible to anyone not opening the tool. When the team moves to a spec-driven workflow, the question is how to keep the genuinely dynamic parts without losing them in the migration. The answer is to declare them rather than script them.
Imperative scripts versus declarative configuration
The two approaches represent the same capability with very different maintenance properties.
| Dimension | Pre-request script | Declarative spec/config |
|---|---|---|
| Form | Imperative code | Declarative data |
| Portability | Locked to one tool | Usable across clients |
| Reviewability | Hidden in collection | Diffable in the repo |
| Testing | Only at send time | Validated in CI |
| Reuse | Copy per collection | Defined once |
Declare how a dynamic value is produced
A signature or a timestamp can be expressed as a binding that names the algorithm and the target header, so the client computes it at send time from a reviewed definition rather than ad hoc script.
# declarative request configuration; the client computes the value
# at send time, but the rule is data, not a tool-specific script
requests:
createOrder:
method: POST
path: /v1/orders
headers:
x-request-timestamp:
source: now
format: unix
x-body-signature:
source: hmac
algorithm: sha256
secret: ${env.WEBHOOK_SECRET}
over: rawBody
idempotency-key:
source: uuid
The same definitions generate the correct behavior in every client, and the secret is referenced from the environment rather than embedded in the script. Reviewing the request logic becomes a diff in the repository instead of a click through the collection UI.
Handle response chaining declaratively too
Values extracted from a previous response, a common use of scripts, can be declared as bindings between operations so the flow is explicit and type-checked rather than stored in loosely typed collection variables.
flow:
- operation: createOrder
capture:
orderId: response.body.id
- operation: captureOrder
with:
orderId: ${createOrder.orderId}
# the client knows the shape of orderId from the schema, instead of
# reading an untyped variable set by an earlier script
Validate the dynamic behavior in CI
Because the rules are declarative and live in the repo, a build can verify them: the signature algorithm is supported, the secret reference resolves, and the captured fields exist in the schema. This turns the "it works on my machine" nature of pre-request scripts into tested, portable behavior. A local-first OpenAPI workflow keeps these definitions next to the contract and generates the client from both, so dynamic requests stay powerful without becoming an undocumented layer of imperative code.
Frequently asked questions
Are there cases where a script is still necessary?
For genuinely one-off or highly custom logic, a small script may be simpler, but prefer a declarative binding for anything reusable, and keep custom code in a reviewed, testable module rather than embedded in a collection.
How do signatures stay secure without scripting?
The declarative rule names the algorithm and references the secret from the environment, so the client still computes the signature at send time, but the secret handling and algorithm are reviewed and consistent.
What happens to existing collection variables?
Migrate them to declared bindings between operations, which gives type information and makes the dependency between requests explicit instead of implicit.
Does this make simple requests harder to set up?
For static requests there is no added complexity; the declarative form only describes the dynamic parts that a script would otherwise have handled, and those are precisely the parts worth making explicit.
This is part of the Powerduck vs Hoppscotch comparison — start there for the full decision around collection-first and spec-first workflows.