<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>Swastik Codes blog</title>
<link>https://swastik.codes/blog/</link>
<atom:link href="https://swastik.codes/feed.xml" rel="self" type="application/rss+xml"/>
<description>Guides to testing and building APIs with Swastik Codes.</description>
<language>en</language>
<lastBuildDate>Sat, 24 Oct 2026 03:30:00 GMT</lastBuildDate>
<item>
<title>Chain API requests using values from the previous response</title>
<link>https://swastik.codes/blog/chain-api-requests/</link>
<guid isPermaLink="true">https://swastik.codes/blog/chain-api-requests/</guid>
<pubDate>Fri, 23 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>API testing</category><category>Scripts</category><category>Variables</category><category>Tutorial</category>
<description>Pass a token or id from one API response into the next request with swc.* scripts and variables, then run the chain in order with the collection runner.</description>
<content:encoded><![CDATA[<p>To use a value from one API response in the next request, add a post-response script to the first request that reads the value from the response and saves it in a variable, for example <code>swc.collectionVariables.set('userId', swc.response.json().id)</code>. Then reference the variable as <code>{{userId}}</code> in the URL, headers, body or auth of the next request. Save both requests in a collection and run it with the collection runner, and the value is passed along automatically every time.</p>
<p>This guide builds a working chain step by step, shows the common patterns (login tokens, created ids, pagination, polling) and explains the variable rules that make chains work or quietly break. The examples use <code>swc.*</code>, Swastik Codes' scripting API; it is the same object as Postman's <code>pm.*</code>, so you can write <code>pm.</code> instead and Postman scripts run unchanged.</p>
<h2 id="why-chain-requests">Why chain requests</h2>
<p>Real API workflows are rarely a single call:</p>
<ul><li><strong>Log in, then call protected endpoints</strong> with the token from the login response.</li><li><strong>Create, then read, update and delete</strong> the resource using the id the create call returned.</li><li><strong>Follow pagination</strong> using the cursor or next-page URL from each page.</li><li><strong>Start a job, then poll</strong> until its status says it is finished.</li></ul>
<p>Copying values from one response into the next request by hand works once. It does not work in a test suite you run every day, in CI or on a schedule. Chaining makes the sequence repeatable.</p>
<h2 id="the-building-blocks">The building blocks</h2>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Piece</th><th>What it does</th></tr></thead><tbody><tr><td><code>swc.response.json()</code></td><td>parses the response body so you can read fields from it</td></tr><tr><td><code>swc.response.headers.get('Location')</code></td><td>reads a response header</td></tr><tr><td><code>swc.collectionVariables.set(key, value)</code></td><td>stores a value with the collection</td></tr><tr><td><code>swc.environment.set(key, value)</code></td><td>stores a value in the active environment</td></tr><tr><td><code>{{key}}</code></td><td>inserts a variable into a URL, header, body or auth field</td></tr><tr><td>collection runner</td><td>sends saved requests in order and keeps variables flowing between them</td></tr></tbody></table></div>
<p>Variables resolve from the narrowest scope that defines them: local, then data (in a data-driven run), environment, collection, then global. The <a href="https://swastik.codes/features/environments-variables/">environments and variables</a> page describes the scopes in full.</p>
<h2 id="step-by-step-a-three-request-chain">Step by step: a three-request chain</h2>
<p>This example uses JSONPlaceholder, a free fake REST API, so you can follow along in <a href="https://swastik.codes/app">the app</a> with a <a href="https://swastik.codes/signup">free account</a>. The chain finds a user by username, lists that user's posts, then loads the comments on the first post.</p>
<h3 id="1-set-up-a-collection-and-a-base-url">1. Set up a collection and a base URL</h3>
<p>Create a collection called <em>Chaining demo</em>. In its <strong>Variables</strong> tab add:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Variable</th><th>Initial value</th></tr></thead><tbody><tr><td><code>baseUrl</code></td><td><code>https://jsonplaceholder.typicode.com</code></td></tr><tr><td><code>userId</code></td><td><em>(empty)</em></td></tr><tr><td><code>postId</code></td><td><em>(empty)</em></td></tr></tbody></table></div>
<p>Declaring <code>userId</code> and <code>postId</code> up front is optional but worth doing, for a reason explained in the section on variable rules below.</p>
<h3 id="2-first-request-find-the-user-and-save-the-id">2. First request: find the user and save the id</h3>
<p>Create a GET request to <code>{{baseUrl}}/users?username=Bret</code>, save it in the collection as <em>Find user</em>, and add this <strong>Post-response</strong> script in the <strong>Scripts</strong> tab:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const users = swc.response.json();

swc.test('user was found', function () {
  swc.response.to.have.status(200);
  swc.expect(users).to.be.an('array').with.lengthOf(1);
});

swc.collectionVariables.set('userId', users[0].id);</code></pre></div>
<p>Send it. The API returns an array with one user whose <code>id</code> is <code>1</code>, and the script stores <code>1</code> in the collection variable <code>userId</code>. Open the collection's Variables tab and you will see the new value.</p>
<h3 id="3-second-request-use-the-id-save-the-next-one">3. Second request: use the id, save the next one</h3>
<p>Create <em>List posts</em>: GET <code>{{baseUrl}}/posts?userId={{userId}}</code>. The variable is highlighted in the URL, and shown in red if it does not resolve. Its post-response script:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const posts = swc.response.json();

swc.test('user has posts', function () {
  swc.response.to.have.status(200);
  swc.expect(posts).to.be.an('array').that.is.not.empty;
  swc.expect(posts.every(function (p) { return p.userId === Number(swc.collectionVariables.get('userId')); })).to.be.true;
});

swc.collectionVariables.set('postId', posts[0].id);</code></pre></div>
<h3 id="4-third-request-use-both">4. Third request: use both</h3>
<p>Create <em>Get comments</em>: GET <code>{{baseUrl}}/posts/{{postId}}/comments</code>, with a test:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('comments belong to the post', function () {
  const comments = swc.response.json();
  swc.expect(comments).to.not.be.empty;
  comments.forEach(function (c) {
    swc.expect(c.postId).to.eql(Number(swc.collectionVariables.get('postId')));
  });
});</code></pre></div>
<p>The tests convert with <code>Number(...)</code> so the comparison works whether the stored value comes back as a number or as text.</p>
<h3 id="5-run-the-chain">5. Run the chain</h3>
<p>Open the collection's menu in the sidebar and choose <strong>Run collection</strong>. The <a href="https://swastik.codes/features/collection-runner/">collection runner</a> shows the run order (the order of the requests in the collection, which you can change for the run), sends each request, runs its scripts and passes the updated variables to the next request. The summary shows every test for all three requests. Export it as JSON, JUnit XML or HTML if you need to share the result.</p>
<h2 id="pattern-log-in-and-reuse-the-token">Pattern: log in and reuse the token</h2>
<p>The most common chain is authentication. Suppose your API has <code>POST /auth/login</code> that returns <code>{&quot;access_token&quot;: &quot;...&quot;, &quot;expires_in&quot;: 3600}</code>.</p>
<p>The <em>Login</em> request (with <strong>No auth</strong> in its Authorization tab, since it is how you get credentials) has a JSON body that uses environment variables:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{
  &quot;email&quot;: &quot;{{email}}&quot;,
  &quot;password&quot;: &quot;{{password}}&quot;
}</code></pre></div>
<p>and a post-response script:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('login succeeded', function () {
  swc.response.to.have.status(200);
});

const body = swc.response.json();
swc.environment.set('token', body.access_token);
swc.environment.set('tokenExpiresAt', Date.now() + body.expires_in * 1000);</code></pre></div>
<p>Then open the collection, set its Authorization to <strong>Bearer token</strong> with the value <code>{{token}}</code>, and leave the other requests on <strong>Inherit auth from parent</strong>. Every request after <em>Login</em> is sent with the fresh token. Put <em>Login</em> first in the collection so the runner sends it first. The <a href="https://swastik.codes/blog/test-jwt-authentication/">JWT authentication testing guide</a> covers what to assert about the token itself.</p>
<p><code>swc.environment.set</code> writes to the active environment, so select one before you run. If no environment is active, use <code>swc.collectionVariables.set</code> instead.</p>
<h3 id="fetching-the-token-only-when-needed">Fetching the token only when needed</h3>
<p>Instead of a separate login request, a collection-level <strong>Pre-request</strong> script can fetch a token with <code>swc.sendRequest</code> whenever there is none or it has expired. Collection scripts run before every request in the collection:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const expiresAt = Number(swc.environment.get('tokenExpiresAt') || 0);

if (!swc.environment.get('token') || Date.now() &gt; expiresAt - 30000) {
  swc.sendRequest({
    url: swc.environment.get('baseUrl') + '/auth/login',
    method: 'POST',
    header: [{ key: 'Content-Type', value: 'application/json' }],
    body: {
      mode: 'raw',
      raw: JSON.stringify({ email: swc.environment.get('email'), password: swc.environment.get('password') })
    }
  }, function (err, res) {
    if (err) { console.log(err); return; }
    const body = res.json();
    swc.environment.set('token', body.access_token);
    swc.environment.set('tokenExpiresAt', Date.now() + body.expires_in * 1000);
  });
}</code></pre></div>
<p>Requests made with <code>swc.sendRequest</code> go through the same network rules as normal sends and show up in the console, so you can see what the script sent. For standard OAuth 2.0 flows the built-in OAuth 2.0 auth type is simpler, since it gets and refreshes tokens without a script; see <a href="https://swastik.codes/blog/oauth2-in-api-client/">OAuth 2.0 in an API client</a>.</p>
<h2 id="pattern-create-then-read-update-and-delete">Pattern: create, then read, update and delete</h2>
<p>A create call gives you the id the rest of the CRUD chain needs. Many APIs return it in the body; some only in the <code>Location</code> header.</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// Post-response script of &quot;Create order&quot;
swc.test('order created', function () {
  swc.response.to.have.status(201);
});

const order = swc.response.json();
swc.collectionVariables.set('orderId', order.id);

// If the API only sends a Location header such as /orders/ord_123:
const location = swc.response.headers.get('Location');
if (location) {
  swc.collectionVariables.set('orderUrl', location);
}</code></pre></div>
<p>The following requests use <code>{{baseUrl}}/orders/{{orderId}}</code>: GET to confirm it exists, PATCH to change it, DELETE to remove it, and a final GET that expects <code>404</code>. That last request is easy to forget and catches APIs that only pretend to delete. If you are unsure which codes to expect at each step, see <a href="https://swastik.codes/blog/http-status-codes/">HTTP status codes explained</a>.</p>
<h2 id="pattern-values-inside-json-bodies">Pattern: values inside JSON bodies</h2>
<p>Variables work inside request bodies too. When the next request needs a value in its body:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{
  &quot;orderId&quot;: &quot;{{orderId}}&quot;,
  &quot;quantity&quot;: 2,
  &quot;note&quot;: &quot;created by run at {{$isoTimestamp}}&quot;
}</code></pre></div>
<p><code>{{$isoTimestamp}}</code> is a dynamic variable, generated fresh on each send, alongside others such as <code>{{$guid}}</code> and <code>{{$randomInt}}</code>. If you need a value that is not a plain string, such as an array or an object from the previous response, store it as JSON and insert it without quotes:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.collectionVariables.set('itemIds', JSON.stringify(swc.response.json().items.map(function (i) { return i.id; })));</code></pre></div>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{ &quot;ids&quot;: {{itemIds}} }</code></pre></div>
<h2 id="pattern-loops-and-polling-with-setnextrequest">Pattern: loops and polling with setNextRequest</h2>
<p>The runner normally goes top to bottom. A script can change that with <code>swc.execution.setNextRequest('Request name')</code>, which jumps to the named request next, or <code>swc.execution.setNextRequest(null)</code>, which ends the iteration. This is how you follow pagination:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// Post-response script of &quot;List orders&quot; (URL: {{baseUrl}}/orders?cursor={{cursor}})
const page = swc.response.json();

if (page.next_cursor) {
  swc.collectionVariables.set('cursor', page.next_cursor);
  swc.execution.setNextRequest('List orders');
} else {
  swc.collectionVariables.set('cursor', '');
}</code></pre></div>
<p>Polling a background job looks the same: repeat <em>Check job status</em> until the job reports done, then jump on to <em>Download result</em>. Add a delay between requests in the runner settings so you poll politely, and make sure the loop has an exit, because a job that never finishes would otherwise keep the run going until you stop it. <code>setNextRequest</code> is meant for collection runs and monitors; it does not change anything when you click Send on a single request.</p>
<h2 id="variable-rules-that-decide-whether-a-chain-works">Variable rules that decide whether a chain works</h2>
<p><strong>Which scope to use.</strong> Use collection variables for values that belong to one API's test flow (ids, cursors). Use environment variables for values that differ between staging and production, such as base URLs and credentials, and for tokens tied to those credentials. <code>swc.variables.set</code> sets a local variable: when you click Send it lasts only for that request, so it is the wrong choice for passing a value to a request you send later by hand.</p>
<p><strong>Shared versus personal values.</strong> Every variable has an initial value, shared with your team, and a current value, which stays on your device and is never stored on the server. When a script updates a variable that already exists, it changes the current value. When a script creates a variable that does not exist yet, the value becomes its initial value, which is shared when the collection or environment syncs. So declare <code>token</code> (with an empty initial value) before your scripts set it, and a real token never ends up in the shared copy.</p>
<p><strong>Single sends versus runs.</strong> When you click Send, changes to environment, collection and global variables are written back as soon as the scripts finish, so you can run a chain by hand, one request after another. Collection variables are only written back when the request is saved in that collection. In the collection runner, changes always flow from one request to the next within an iteration. The <strong>Persist variables between requests</strong> option (on by default) also keeps them between iterations and writes them back to your environment after the run; turn it off when every iteration should start from the same initial values.</p>
<p><strong>Debugging.</strong> If a request is sent with an empty value, open the console panel. It shows the request exactly as sent, after variable substitution, plus anything your scripts logged. Logging the value you extract is often the fastest check:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">console.log('saving userId', users[0] &amp;&amp; users[0].id);</code></pre></div>
<h2 id="chaining-without-scripts-flows">Chaining without scripts: Flows</h2>
<p>If you prefer not to write scripts, <a href="https://swastik.codes/features/flows/">Flows</a> chain saved requests on a visual canvas. Each node's output becomes the next node's input: after a login request, a Select node on <code>body.token</code> picks out the token, and the next request uses it as <code>{{input}}</code>, for example in an <code>Authorization: Bearer {{input}}</code> header. Condition, Loop and Delay nodes handle branching and polling. Flows suit workflows that branch or loop over data, while the collection runner with scripts suits test suites with assertions and reports. Both use your saved requests.</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th></th><th>Scripts + collection runner</th><th>Flows</th></tr></thead><tbody><tr><td>How values are passed</td><td>variables set in scripts</td><td>node output feeds the next node</td></tr><tr><td>Branching and loops</td><td><code>setNextRequest</code></td><td>Condition and Loop nodes</td></tr><tr><td>Input</td><td>CSV or JSON data files, one iteration per row</td><td>one input per run (JSON or text)</td></tr><tr><td>Reports</td><td>JSON, JUnit XML, HTML</td><td>per-node run log</td></tr><tr><td>Triggered by</td><td>Run button, monitors</td><td>Run button, webhook</td></tr></tbody></table></div>
<h2 id="common-mistakes">Common mistakes</h2>
<ul><li><strong>Running requests in the wrong order.</strong> The runner follows the collection order unless you reorder it or use <code>setNextRequest</code>. Put the request that produces a value before the one that uses it.</li><li><strong>No active environment.</strong> <code>swc.environment.set</code> has nowhere to write. Select an environment or use collection variables.</li><li><strong>Reading the body of an error.</strong> If login fails, <code>swc.response.json().access_token</code> is <code>undefined</code>, and every later request fails with <code>401</code>. Test the status before extracting, so the first failure explains the rest.</li><li><strong>A stale value from an earlier run.</strong> A variable keeps its last value. Clear it at the start of a run (<code>swc.collectionVariables.unset('orderId')</code> in the first request's pre-request script) if a leftover value could hide a failure.</li></ul>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="how-do-i-use-a-response-value-in-the-next-request">How do I use a response value in the next request?</h3>
<p>In the first request's post-response script, read the value and save it: <code>swc.collectionVariables.set('id', swc.response.json().id)</code>. In the next request, use <code>{{id}}</code> wherever the value is needed.</p>
<h3 id="do-postman-pm-environment-set-scripts-work-for-chaining">Do Postman <code>pm.environment.set</code> scripts work for chaining?</h3>
<p>Yes. <code>pm</code> and <code>swc</code> are the same object in Swastik Codes, so <code>pm.environment.set</code>, <code>pm.collectionVariables.set</code> and <code>pm.response.json()</code> behave the same way. Imported Postman collections keep their chains.</p>
<h3 id="how-do-i-save-a-value-from-a-response-header">How do I save a value from a response header?</h3>
<p>Use <code>swc.response.headers.get('Header-Name')</code>, for example <code>swc.collectionVariables.set('orderUrl', swc.response.headers.get('Location'))</code>.</p>
<h3 id="can-i-chain-requests-without-running-the-whole-collection">Can I chain requests without running the whole collection?</h3>
<p>Yes. Variables set by scripts are saved when you click Send, so you can send the requests one at a time in order. Use the collection runner when you want the chain to run unattended, or a folder run to run only part of it.</p>
<h3 id="can-i-run-a-chain-of-requests-on-a-schedule">Can I run a chain of requests on a schedule?</h3>
<p>Yes. Attach the collection to a <a href="https://swastik.codes/features/api-monitoring/">monitor</a> and it runs on a cron schedule with the same scripts, alerting you by email or webhook when a test fails.</p>]]></content:encoded>
</item>
<item>
<title>Convert cURL to Python, JavaScript and other code</title>
<link>https://swastik.codes/blog/convert-curl-to-code/</link>
<guid isPermaLink="true">https://swastik.codes/blog/convert-curl-to-code/</guid>
<pubDate>Fri, 23 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>cURL</category><category>Code generation</category><category>Python</category><category>JavaScript</category>
<description>Paste a cURL command, get an editable request, then generate Python requests, fetch, Go or 27 more code targets. Every common cURL flag explained.</description>
<content:encoded><![CDATA[<p>To convert a cURL command to Python, JavaScript or another language, parse it into its parts (method, URL, headers, body, auth and options) and rewrite each part with the target library. In Swastik Codes you paste the cURL command into the URL bar or the import dialog, which turns it into an editable request, then open the <strong>Code snippet</strong> panel and pick one of 30 targets, including Python Requests, JavaScript fetch, Axios, Go, Java and C#. This guide shows both steps and explains what each common cURL flag means in Python <code>requests</code> and <code>fetch</code>, so you can also convert by hand.</p>
<h2 id="step-1-get-a-curl-command">Step 1: get a cURL command</h2>
<p>You usually already have one: from API documentation, a teammate's message, a terminal history or a bug report. If the request comes from a website or web app, the browser can produce one for you.</p>
<h3 id="copy-as-curl-from-browser-devtools">Copy as cURL from browser devtools</h3>
<ul><li><strong>Chrome and Edge</strong>: open DevTools (F12), go to the <strong>Network</strong> tab, reload or trigger the request, right-click it and choose <strong>Copy → Copy as cURL</strong>. On Windows there are two variants, <strong>Copy as cURL (cmd)</strong> and <strong>Copy as cURL (bash)</strong>.</li><li><strong>Firefox</strong>: in the <strong>Network</strong> tab, right-click the request and choose <strong>Copy Value → Copy as cURL</strong>. On Windows, Firefox also offers POSIX and Windows variants.</li><li><strong>Safari</strong>: enable the Develop menu, open Web Inspector, go to <strong>Network</strong>, right-click the request and choose <strong>Copy as cURL</strong>.</li></ul>
<p>A copied command contains every header the browser sent, including <code>Cookie</code> and <code>Authorization</code>. Treat it like a password: do not paste it into public issues or chat channels, and remove headers you do not need (<code>sec-ch-ua</code>, <code>sec-fetch-*</code>, <code>priority</code> and similar browser-only headers rarely matter to an API).</p>
<p>If you want many requests at once rather than one, save the whole Network log as a HAR file instead; Swastik Codes <a href="https://swastik.codes/features/import-export/">imports HAR files</a> as a collection.</p>
<h2 id="step-2-import-the-curl-command">Step 2: import the cURL command</h2>
<p>There are two ways in.</p>
<p><strong>Paste into the URL bar.</strong> Open a new HTTP request in <a href="https://swastik.codes/app">the app</a> and paste the full command where the URL goes. It is recognised because it starts with <code>curl</code>, and the method, URL, query parameters, headers, body, auth and relevant settings are filled in. A short confirmation appears.</p>
<p><strong>Use the import dialog.</strong> Open the command palette (Ctrl+K or Cmd+K) and run <strong>Import cURL / paste raw text…</strong>, or open <strong>Import</strong> and paste the text. The import dialog is the better choice for commands copied from Windows or for several commands at once:</p>
<ul><li>It understands POSIX quoting (<code>'…'</code>, <code>&quot;…&quot;</code> and <code>$'…'</code> strings) and <code>` line continuations, the </code>cmd<code> flavour with </code>^` escapes that browsers produce on Windows, and PowerShell backtick continuations.</li><li>Several commands in one paste become one collection with a request per command.</li><li>Anything it cannot carry over is reported as a warning instead of being dropped silently, for example a proxy (<code>-x</code>), client certificates (<code>--cert</code>, <code>--key</code>, <code>--cacert</code>) or a cookie file. Proxies and certificates are configured in the app's settings instead.</li></ul>
<p>Here is a typical command to try:</p>
<div class="code"><div class="code-head"><span>Shell</span></div><pre tabindex="0"><code class="language-bash">curl -X POST 'https://httpbin.org/post?source=docs' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer abc123' \
  --data-raw '{&quot;sku&quot;:&quot;A-1&quot;,&quot;quantity&quot;:2}' \
  --compressed</code></pre></div>
<p>After the import you have a POST request with a <code>source=docs</code> query parameter, two headers and a raw JSON body. Send it to check that it behaves as in your terminal. Sending from the web app needs a free account, and the target must be publicly reachable; for <code>localhost</code> see <a href="https://swastik.codes/blog/test-localhost-api/">how to test a localhost API</a>.</p>
<h2 id="how-each-curl-flag-is-imported">How each cURL flag is imported</h2>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>cURL flag</th><th>What it means in curl</th><th>What the import does</th></tr></thead><tbody><tr><td><code>-X</code>, <code>--request</code></td><td>Sets the method</td><td>Sets the request method</td></tr><tr><td><code>-H</code>, <code>--header</code></td><td>Adds a header</td><td>Adds a row on the <strong>Headers</strong> tab</td></tr><tr><td><code>-d</code>, <code>--data</code></td><td>Sends data, implies POST; default type <code>application/x-www-form-urlencoded</code></td><td>Body as form fields, or raw JSON/text when the data is JSON or a content type says so</td></tr><tr><td><code>--data-raw</code></td><td>Like <code>-d</code>, but <code>@</code> is not treated as a file name</td><td>Same as <code>-d</code>, value kept as typed</td></tr><tr><td><code>--data-binary</code></td><td>Sends data exactly, no newline stripping</td><td>Raw body, or a file body for <code>@file</code></td></tr><tr><td><code>--data-urlencode</code></td><td>URL-encodes the value</td><td>Form field, encoded</td></tr><tr><td><code>--json</code></td><td>JSON body plus JSON <code>Content-Type</code> and <code>Accept</code></td><td>Raw JSON body and both headers</td></tr><tr><td><code>-F</code>, <code>--form</code></td><td><code>multipart/form-data</code> field; <code>@file</code> attaches a file</td><td>form-data body with text and file fields</td></tr><tr><td><code>-u</code>, <code>--user</code></td><td>Basic auth credentials</td><td><strong>Authorization</strong> tab set to Basic</td></tr><tr><td><code>--oauth2-bearer</code></td><td>Bearer token</td><td><strong>Authorization</strong> tab set to Bearer</td></tr><tr><td><code>-b</code>, <code>--cookie</code></td><td>Sends cookies</td><td><code>Cookie</code> header (a cookie file is reported, not read)</td></tr><tr><td><code>-A</code>, <code>-e</code></td><td>User-Agent, Referer</td><td>Matching headers</td></tr><tr><td><code>-G</code>, <code>--get</code></td><td>Puts <code>-d</code> data in the query string</td><td>Data appended to the URL</td></tr><tr><td><code>-I</code>, <code>--head</code></td><td>HEAD request</td><td>Method HEAD</td></tr><tr><td><code>-k</code>, <code>--insecure</code></td><td>Skips TLS certificate verification</td><td>Turns off <strong>Enable SSL certificate verification</strong> for the request</td></tr><tr><td><code>-L</code>, <code>--location</code></td><td>Follows redirects (curl does not by default)</td><td>Redirects are followed (the app's default)</td></tr><tr><td><code>-m</code>, <code>--max-time</code></td><td>Overall time limit in seconds</td><td>Request timeout</td></tr><tr><td><code>--max-redirs</code></td><td>Redirect limit</td><td>Maximum redirects setting</td></tr><tr><td><code>--compressed</code></td><td>Asks for a compressed response and decompresses it</td><td>Accepted; the engine already sends <code>Accept-Encoding</code> and decompresses</td></tr></tbody></table></div>
<p>In the import dialog, <code>--digest</code> and <code>--ntlm</code> next to <code>-u</code> set the auth type to Digest or NTLM, and <code>--http2</code> or <code>--http1.1</code> set the request's HTTP version. Output-only flags such as <code>-s</code>, <code>-v</code>, <code>-i</code>, <code>-o</code> and <code>-w</code> are ignored because they only affect how curl prints.</p>
<p>File references need attention: the web app cannot read a path on your disk, so check file fields in the <strong>Body</strong> tab and select the file there before sending.</p>
<h2 id="step-3-generate-python-javascript-or-other-code">Step 3: generate Python, JavaScript or other code</h2>
<p>With the request open, click the <strong>Code snippet</strong> button in the request toolbar (or run <strong>Generate code snippet</strong> from the command palette). Choose a language and copy or download the snippet. The 30 targets are:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Language</th><th>Targets</th></tr></thead><tbody><tr><td>Shell</td><td>cURL, Wget, HTTPie, PowerShell (Invoke-RestMethod)</td></tr><tr><td>Raw</td><td>HTTP request text</td></tr><tr><td>JavaScript</td><td>Fetch, Axios, jQuery, XHR</td></tr><tr><td>Node.js</td><td>native <code>http</code>, Axios</td></tr><tr><td>Python</td><td>Requests, http.client</td></tr><tr><td>Go</td><td>net/http</td></tr><tr><td>Java</td><td>OkHttp, Unirest, java.net.http</td></tr><tr><td>C#</td><td>HttpClient, RestSharp</td></tr><tr><td>PHP</td><td>cURL, Guzzle</td></tr><tr><td>Ruby</td><td>Net::HTTP</td></tr><tr><td>Swift</td><td>URLSession</td></tr><tr><td>Kotlin</td><td>OkHttp</td></tr><tr><td>Dart</td><td>http</td></tr><tr><td>Rust</td><td>reqwest</td></tr><tr><td>C</td><td>libcurl</td></tr><tr><td>R</td><td>httr, RCurl</td></tr><tr><td>OCaml</td><td>Cohttp</td></tr></tbody></table></div>
<p>Two options matter:</p>
<ul><li><strong>Resolve variables</strong>: on, <code>{{baseUrl}}</code> and <code>{{token}}</code> are replaced with their current values; off, the placeholders stay. Resolved snippets can contain real secrets.</li><li><strong>Snippet settings</strong>: indent with spaces or tabs, the indent width, and whether to trim the request body.</li></ul>
<p>The request's auth is applied in the snippet, including auth inherited from a folder or collection, so a Bearer token becomes an <code>Authorization</code> header. Generating code never sends the request. The <a href="https://swastik.codes/features/code-generation/">code generation</a> page has the details.</p>
<p>The round trip also works the other way: build or fix a request in the app, then pick <strong>cURL</strong> as the target to get a command for a terminal or a bug report.</p>
<h2 id="converting-by-hand-curl-to-python-requests-and-fetch">Converting by hand: cURL to Python requests and fetch</h2>
<p>If you only need one conversion, or want to understand what a generator produces, these are the equivalents for the common flags.</p>
<h3 id="method-headers-and-a-json-body-x-h-data-raw">Method, headers and a JSON body (-X, -H, --data-raw)</h3>
<div class="code"><div class="code-head"><span>Shell</span></div><pre tabindex="0"><code class="language-bash">curl -X POST https://api.example.com/v1/orders \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer abc123' \
  --data-raw '{&quot;sku&quot;:&quot;A-1&quot;,&quot;quantity&quot;:2}'</code></pre></div>
<div class="code"><div class="code-head"><span>Python</span></div><pre tabindex="0"><code class="language-python">import requests

resp = requests.post(
    &quot;https://api.example.com/v1/orders&quot;,
    headers={&quot;Authorization&quot;: &quot;Bearer abc123&quot;},
    json={&quot;sku&quot;: &quot;A-1&quot;, &quot;quantity&quot;: 2},  # json= sets Content-Type: application/json
)
print(resp.status_code, resp.json())</code></pre></div>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const resp = await fetch('https://api.example.com/v1/orders', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'Bearer abc123',
  },
  body: JSON.stringify({ sku: 'A-1', quantity: 2 }),
});
console.log(resp.status, await resp.json());</code></pre></div>
<h3 id="form-data-d">Form data (-d)</h3>
<p><code>curl -d 'name=Ada&amp;role=admin' https://api.example.com/users</code> sends <code>application/x-www-form-urlencoded</code> and uses POST because data is present.</p>
<div class="code"><div class="code-head"><span>Python</span></div><pre tabindex="0"><code class="language-python">requests.post(&quot;https://api.example.com/users&quot;, data={&quot;name&quot;: &quot;Ada&quot;, &quot;role&quot;: &quot;admin&quot;})</code></pre></div>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">await fetch('https://api.example.com/users', {
  method: 'POST',
  body: new URLSearchParams({ name: 'Ada', role: 'admin' }), // sets the form content type
});</code></pre></div>
<h3 id="basic-auth-u">Basic auth (-u)</h3>
<p><code>curl -u ada:secret https://api.example.com/me</code> sends <code>Authorization: Basic</code> with <code>ada:secret</code> Base64-encoded.</p>
<div class="code"><div class="code-head"><span>Python</span></div><pre tabindex="0"><code class="language-python">requests.get(&quot;https://api.example.com/me&quot;, auth=(&quot;ada&quot;, &quot;secret&quot;))</code></pre></div>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// Browser: btoa(); in Node.js use Buffer.from('ada:secret').toString('base64')
await fetch('https://api.example.com/me', {
  headers: { Authorization: 'Basic ' + btoa('ada:secret') },
});</code></pre></div>
<h3 id="file-upload-f">File upload (-F)</h3>
<p><code>curl -F 'title=Report' -F 'file=@report.pdf' https://api.example.com/upload</code> sends <code>multipart/form-data</code>.</p>
<div class="code"><div class="code-head"><span>Python</span></div><pre tabindex="0"><code class="language-python">with open(&quot;report.pdf&quot;, &quot;rb&quot;) as f:
    requests.post(
        &quot;https://api.example.com/upload&quot;,
        data={&quot;title&quot;: &quot;Report&quot;},
        files={&quot;file&quot;: (&quot;report.pdf&quot;, f, &quot;application/pdf&quot;)},
    )</code></pre></div>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const form = new FormData();
form.append('title', 'Report');
form.append('file', fileInput.files[0]); // a File or Blob
await fetch('https://api.example.com/upload', { method: 'POST', body: form });
// Do not set Content-Type yourself: the multipart boundary is added for you.</code></pre></div>
<h3 id="insecure-tls-k">Insecure TLS (-k)</h3>
<div class="code"><div class="code-head"><span>Python</span></div><pre tabindex="0"><code class="language-python">requests.get(&quot;https://dev.internal/health&quot;, verify=False)  # or verify=&quot;/path/to/ca.pem&quot;</code></pre></div>
<p><code>fetch</code> in a browser has no equivalent; the browser decides whether to trust a certificate. In Node.js you can pass a custom dispatcher from the <code>undici</code> package with <code>connect: { rejectUnauthorized: false }</code>. In every language, prefer trusting the specific CA over turning verification off.</p>
<h3 id="redirects-l-and-compression-compressed">Redirects (-L) and compression (--compressed)</h3>
<p>curl does not follow redirects unless you pass <code>-L</code>. Python <code>requests</code> follows them by default (set <code>allow_redirects=False</code> to stop), and <code>fetch</code> follows them by default (<code>redirect: 'manual'</code> or <code>'error'</code> to change that). Both libraries request compressed responses and decompress them automatically, so <code>--compressed</code> usually needs no translation.</p>
<h2 id="common-conversion-mistakes">Common conversion mistakes</h2>
<ul><li><strong>Dropping the method change.</strong> <code>-d</code> or <code>-F</code> without <code>-X</code> still means POST. A converted GET with a body is a frequent bug.</li><li><strong>Sending JSON as form data.</strong> <code>-d '{&quot;a&quot;:1}'</code> without a <code>Content-Type: application/json</code> header is sent as <code>application/x-www-form-urlencoded</code> by curl. Many servers then reject it; add the header (or use <code>--json</code>) in the original command too.</li><li><strong>Setting the multipart Content-Type by hand.</strong> The boundary must match the body, so let the library generate the header.</li><li><strong>Quoting on Windows.</strong> Single quotes do not work in <code>cmd</code>. Use the browser's cmd variant or paste into the import dialog, which handles <code>^</code> escapes.</li><li><strong>Leaking tokens.</strong> Generated snippets with resolved variables, and commands copied from devtools, contain live credentials. Rotate anything that ends up somewhere public.</li></ul>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="how-do-i-convert-a-curl-command-to-python-requests">How do I convert a cURL command to Python requests?</h3>
<p>Paste the command into a Swastik Codes request (URL bar or import dialog), open the <strong>Code snippet</strong> panel and choose <strong>Python - Requests</strong>. By hand: <code>-X</code> becomes the method function, <code>-H</code> the <code>headers</code> dict, <code>-d</code> the <code>data</code> argument (or <code>json</code> for JSON), <code>-u</code> the <code>auth</code> tuple, <code>-F</code> the <code>files</code> argument and <code>-k</code> <code>verify=False</code>.</p>
<h3 id="how-do-i-convert-curl-to-javascript-fetch">How do I convert cURL to JavaScript fetch?</h3>
<p>Import the command, then choose <strong>JavaScript - Fetch</strong> in the code snippet panel. By hand, <code>-X</code> maps to <code>method</code>, <code>-H</code> to <code>headers</code>, <code>-d</code> to <code>body</code> (a string, <code>URLSearchParams</code> or <code>FormData</code>), and Basic auth to an <code>Authorization</code> header you build yourself.</p>
<h3 id="what-is-the-difference-between-d-data-raw-and-data-binary">What is the difference between -d, --data-raw and --data-binary?</h3>
<p><code>-d</code> treats a leading <code>@</code> as a file name and strips newlines from file contents. <code>--data-raw</code> sends the value as typed, even if it starts with <code>@</code>. <code>--data-binary</code> sends data exactly as given, including newlines, which matters for uploads.</p>
<h3 id="can-i-import-several-curl-commands-at-once">Can I import several cURL commands at once?</h3>
<p>Yes. Paste them together into the import dialog and each command becomes a request in one new collection.</p>
<h3 id="does-swastik-codes-convert-requests-back-to-curl">Does Swastik Codes convert requests back to cURL?</h3>
<p>Yes. Choose <strong>cURL</strong> in the code snippet panel. Other shell targets are Wget, HTTPie and PowerShell.</p>
<h3 id="is-my-curl-command-sent-anywhere-when-i-convert-it">Is my cURL command sent anywhere when I convert it?</h3>
<p>The import dialog parses the text with Swastik Codes' own importer (in the web app on the swastik.codes server, with an in-browser fallback; in the desktop app locally). The API named in the command is not called. Generating code never sends the request. Only pressing <strong>Send</strong> calls the API.</p>]]></content:encoded>
</item>
<item>
<title>How to test a localhost API (and why web clients can&apos;t)</title>
<link>https://swastik.codes/blog/test-localhost-api/</link>
<guid isPermaLink="true">https://swastik.codes/blog/test-localhost-api/</guid>
<pubDate>Fri, 23 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>Testing</category><category>Localhost</category><category>Docker</category><category>Desktop app</category>
<description>Why online API clients cannot reach localhost alone, and how to test a local API anyway: a local bridge, a desktop app, a tunnel or a staging deploy.</description>
<content:encoded><![CDATA[<p>To test an API running on localhost, the request has to start on the same machine (or network) as the API, because <code>localhost</code> always means &quot;the computer sending this request&quot;. A web-based client that sends through its own server cannot reach your laptop's <code>localhost</code> on its own. With Swastik Codes you can keep using the web app and run <a href="https://swastik.codes/features/cli/#reach-localhost-from-the-web-app"><code>swc bridge</code></a> on your computer so requests leave from there, use the <a href="https://swastik.codes/download/">desktop app</a> for Windows and Linux, expose the port through a tunnel with a public HTTPS hostname, or deploy to a staging server. If the API runs in Docker, also check that it listens on <code>0.0.0.0</code> and that the port is published.</p>
<p>The rest of this guide explains why the web app cannot do it, walks through each option with concrete commands, and covers the Docker and binding problems that make a working API look unreachable.</p>
<h2 id="why-an-online-api-client-cannot-reach-localhost">Why an online API client cannot reach localhost</h2>
<p><code>localhost</code> is not an address on the internet. It is a name that resolves to the loopback interface, <code>127.0.0.1</code> for IPv4 and <code>::1</code> for IPv6, of whichever machine does the resolving. The same is true, in a wider sense, of private addresses such as <code>192.168.1.20</code> or <code>10.0.0.5</code>: they only mean something inside your network.</p>
<p>A browser page is limited in what it can send. It cannot set some headers, it is subject to CORS, and it cannot open raw TCP connections for protocols such as gRPC or MQTT. So the Swastik Codes web app does not send requests from your browser tab. It sends them to the swastik.codes server, and the server makes the real request and returns the response. That is what makes custom headers, every auth type and eight protocols work in a browser.</p>
<p>When that server sees <code>http://localhost:3000/health</code>, <code>localhost</code> refers to the server itself, not to your laptop. For safety the server refuses private, loopback, link-local and other reserved addresses outright. A request to <code>localhost</code> or to <code>192.168.1.20</code> fails with an error like this:</p>
<div class="code"><div class="code-head"><span>Text</span></div><pre tabindex="0"><code class="language-text">refusing to connect to localhost: it resolves to a private or loopback address</code></pre></div>
<p>The check runs on the resolved IP address, not on the hostname, so a public DNS name that points at <code>127.0.0.1</code> is refused too. In the web app, the error panel then offers the local bridge and the desktop app.</p>
<h3 id="would-calling-localhost-straight-from-the-browser-work-instead">Would calling localhost straight from the browser work instead?</h3>
<p>Sometimes, but it is fragile. A page served from a public HTTPS origin that calls <code>http://localhost:3000</code> needs your API to send CORS headers that allow that origin, and browsers increasingly restrict public sites from reaching local and private addresses (Chrome's Private Network Access work and the newer local network access permission). Many headers cannot be set from page JavaScript at all. A client that runs outside the browser avoids all of this.</p>
<h2 id="option-1-keep-the-web-app-and-run-the-local-bridge">Option 1: keep the web app and run the local bridge</h2>
<p>The <code>swc</code> command-line tool has a <code>bridge</code> command. It connects out from your computer to swastik.codes with your API key, and while it runs, the requests and collection runs you send from the web app are executed on your computer instead of our server:</p>
<div class="code"><div class="code-head"><span>Shell</span></div><pre tabindex="0"><code class="language-bash">swc login          # paste an API key from Account settings → API keys
swc bridge</code></pre></div>
<div class="code"><div class="code-head"><span>Text</span></div><pre tabindex="0"><code class="language-text">swc bridge: requests sent from https://swastik.codes/app as you@example.com run on this computer (&quot;dev-laptop&quot;).
✓ connected; waiting for requests
  GET http://localhost:3000/health → 200 (4 ms)
  POST http://192.168.1.20:8080/orders → 201 (18 ms)</code></pre></div>
<p>Then click <strong>Local bridge</strong> in the app's status bar and switch it on. There is no browser extension and no CORS to configure, and nothing on your machine is opened to the internet: the bridge only takes work from your own signed-in sessions. It covers HTTP requests (REST, GraphQL queries) and collection runs; for WebSocket, gRPC, MQTT and load tests, use the desktop app. <a href="https://swastik.codes/features/cli/#install">Install <code>swc</code></a> as a single file for Linux or Windows.</p>
<h2 id="option-2-use-the-desktop-app">Option 2: use the desktop app</h2>
<p>The <a href="https://swastik.codes/download/">Swastik Codes desktop app</a> runs the same interface, but requests leave from your own computer through a native Rust HTTP engine instead of the swastik.codes server. Loopback and private addresses are allowed, so all of these work:</p>
<div class="code"><div class="code-head"><span>Text</span></div><pre tabindex="0"><code class="language-text">GET  http://localhost:3000/health
POST http://192.168.1.20:8080/orders
WS   ws://127.0.0.1:9001/live
gRPC localhost:50051
MQTT mqtt://10.0.0.5:1883</code></pre></div>
<p>What you get:</p>
<ul><li><strong>Every protocol locally</strong>: HTTP requests, collection runs, load tests, and WebSocket, Socket.IO, Server-Sent Events, gRPC, MQTT and MCP connections all start on your machine. The target sees your IP address, and your VPN, hosts file and network rules apply.</li><li><strong>No CORS</strong>: CORS is a rule browsers enforce on pages. A native client is not a page, so the local server does not need CORS headers for your tests to work.</li><li><strong>No account required</strong>: you can work in a local workspace without signing in. Signing in syncs your cloud workspaces, environments and teams between the desktop app and the browser.</li><li><strong>Platforms</strong>: Windows 10 or 11 (64-bit) and Linux x86_64 (<code>.deb</code>, <code>.rpm</code> or AppImage). A macOS version is not available yet; on a Mac, use one of the other options below.</li></ul>
<p>The desktop app is free, like the web app.</p>
<h3 id="a-quick-check-against-a-local-server">A quick check against a local server</h3>
<p>Start any server on port 3000. A minimal one with Node.js:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-js">// server.mjs
import { createServer } from 'node:http';

createServer((req, res) =&gt; {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ ok: true, path: req.url }));
}).listen(3000, '127.0.0.1', () =&gt; console.log('http://127.0.0.1:3000'));</code></pre></div>
<p>Run <code>node server.mjs</code>, open the desktop app, send <code>GET http://localhost:3000/health</code>, and you should see <code>{&quot;ok&quot;:true,&quot;path&quot;:&quot;/health&quot;}</code>. Add a post-response test so the check is repeatable:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('local API is up', function () {
  swc.response.to.have.status(200);
  swc.expect(swc.response.json().ok).to.eql(true);
});</code></pre></div>
<p>Postman's <code>pm.*</code> syntax runs unchanged, so existing scripts work too; see <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">writing API tests with swc.* scripts</a>.</p>
<h3 id="keep-one-collection-for-local-staging-and-production">Keep one collection for local, staging and production</h3>
<p>Put the base URL in a variable rather than in every request: <code>{{baseUrl}}/health</code>. Create an environment per target, for example <code>local</code> with <code>baseUrl = http://localhost:3000</code> and <code>staging</code> with <code>baseUrl = https://staging.example.com</code>, and switch between them. The <a href="https://swastik.codes/features/environments-variables/">environments and variables</a> page explains scopes. In the web app, choose the staging environment; in the desktop app, either works.</p>
<h3 id="https-on-localhost">HTTPS on localhost</h3>
<p>If your local server uses a self-signed certificate or one from a local CA (for example created with <code>mkcert</code>), the request fails certificate verification by default. Either turn off <strong>Enable SSL certificate verification</strong> in the request's <strong>Settings</strong> tab for that request, or add your local CA certificate under <strong>Settings → Certificates</strong> so verification keeps working. The second option is better if you test HTTPS behaviour itself.</p>
<h2 id="option-3-expose-localhost-through-a-tunnel">Option 3: expose localhost through a tunnel</h2>
<p>A tunnel gives your local port a temporary public HTTPS hostname. Traffic to that hostname goes to the tunnel provider and then down a connection your machine opened, so nothing on your router needs to change. This is useful when the client must be the web app, when a teammate on another network needs to call your API, or when a third-party service has to send you a webhook.</p>
<p>Two widely used tools:</p>
<div class="code"><div class="code-head"><span>Shell</span></div><pre tabindex="0"><code class="language-bash"># ngrok (needs a free account and an auth token)
ngrok http 3000

# Cloudflare Tunnel, quick mode (prints a *.trycloudflare.com URL)
cloudflared tunnel --url http://localhost:3000</code></pre></div>
<p>Each prints a public <code>https://</code> URL. Put that URL in your <code>baseUrl</code> variable and send from the web app as usual. Because the hostname resolves to a public address, the private-address check does not apply.</p>
<p>Things to know before you rely on a tunnel:</p>
<ul><li><strong>It is public.</strong> Anyone who learns the URL can call your API. Keep authentication on, avoid real data, and stop the tunnel when you are done.</li><li><strong>Host header checks.</strong> Some development servers reject requests whose <code>Host</code> header they do not recognise. Vite (<code>server.allowedHosts</code>) and webpack-dev-server (<code>allowedHosts</code>) are common examples; add the tunnel hostname there.</li><li><strong>URLs change.</strong> Free or quick tunnels often get a new hostname each time they start. Keep it in a variable so you change it once.</li><li><strong>Extra latency and limits.</strong> Requests travel through the provider, so response times are not representative, and providers apply their own rate and size limits. Do not load-test through a tunnel.</li><li><strong>WebSocket and gRPC.</strong> Tunnels that support WebSocket work for <code>wss://</code> testing. gRPC needs end-to-end HTTP/2, which not every tunnel mode provides; check your provider's documentation.</li></ul>
<h2 id="option-4-deploy-to-a-staging-environment">Option 4: deploy to a staging environment</h2>
<p>For anything beyond a quick check, a staging deployment is the most realistic target: real TLS, a real domain, real proxies and load balancers in front of the service. It is also the only option that lets you run <a href="https://swastik.codes/features/api-monitoring/">scheduled monitors</a> and share a stable URL with the team. A small VM or a platform service is enough. Many problems that never appear on localhost, such as a proxy stripping a header, a body size limit or a missing CORS rule for your frontend, show up here first.</p>
<h2 id="docker-networking-gotchas">Docker networking gotchas</h2>
<p>Many &quot;cannot reach localhost&quot; reports are not client problems at all. The API is running, but not where the client is looking.</p>
<h3 id="the-server-listens-on-127-0-0-1-inside-a-container">The server listens on 127.0.0.1 inside a container</h3>
<p>Inside a container, <code>127.0.0.1</code> is the container's own loopback interface. Docker's published ports forward traffic to the container's network interface, not to its loopback. A server that binds only to <code>127.0.0.1</code> inside the container is therefore unreachable through <code>-p 3000:3000</code>, and the request usually fails with a connection reset or an empty reply.</p>
<p>Bind to all interfaces inside the container:</p>
<div class="code"><div class="code-head"><span>Shell</span></div><pre tabindex="0"><code class="language-bash"># Node / Express
app.listen(3000, '0.0.0.0')

# Python
uvicorn main:app --host 0.0.0.0 --port 8000
flask run --host 0.0.0.0

# Vite dev server
vite --host 0.0.0.0</code></pre></div>
<p><code>0.0.0.0</code> is a bind address meaning &quot;every interface&quot;. It is not a destination; send your requests to <code>localhost</code> or the machine's IP, not to <code>0.0.0.0</code>.</p>
<h3 id="the-port-is-not-published">The port is not published</h3>
<p><code>EXPOSE 3000</code> in a Dockerfile documents the port; it does not make it reachable from the host. Publish it when you run the container:</p>
<div class="code"><div class="code-head"><span>Shell</span></div><pre tabindex="0"><code class="language-bash">docker run -p 3000:3000 my-api           # host port 3000 -&gt; container port 3000
docker run -p 127.0.0.1:3000:3000 my-api # only reachable from this machine</code></pre></div>
<p>With Docker Compose, use <code>ports: [&quot;3000:3000&quot;]</code>. Run <code>docker ps</code> and check the <code>PORTS</code> column: <code>0.0.0.0:3000-&gt;3000/tcp</code> means it is published.</p>
<h3 id="the-api-is-in-a-container-and-calls-something-on-the-host">The API is in a container and calls something on the host</h3>
<p>From inside a container, <code>localhost</code> is the container. To reach a service on the host machine, use <code>host.docker.internal</code>. Docker Desktop provides that name automatically. On Linux with Docker Engine, add it yourself:</p>
<div class="code"><div class="code-head"><span>Shell</span></div><pre tabindex="0"><code class="language-bash">docker run --add-host=host.docker.internal:host-gateway my-api</code></pre></div>
<div class="code"><div class="code-head"><span>YAML</span></div><pre tabindex="0"><code class="language-yaml"># docker-compose.yml
services:
  api:
    extra_hosts:
      - &quot;host.docker.internal:host-gateway&quot;</code></pre></div>
<h3 id="two-containers-talking-to-each-other">Two containers talking to each other</h3>
<p>Containers on the same user-defined network (every Compose project gets one) reach each other by service name: <code>http://db:5432</code>, <code>http://api:3000</code>. Neither <code>localhost</code> nor the host's published port is the right address between containers.</p>
<h3 id="the-server-listens-on-127-0-0-1-and-you-test-from-another-device">The server listens on 127.0.0.1 and you test from another device</h3>
<p>The reverse case: a server bound to <code>127.0.0.1</code> on your laptop is unreachable from your phone, another computer or a VM. Bind to <code>0.0.0.0</code> (or the LAN interface) and allow the port through the firewall, then call <code>http://&lt;laptop-ip&gt;:3000</code>.</p>
<h3 id="quick-diagnosis">Quick diagnosis</h3>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Symptom</th><th>Likely cause</th><th>Fix</th></tr></thead><tbody><tr><td>&quot;refusing to connect … private or loopback address&quot;</td><td>Sending from the web app</td><td>Desktop app, tunnel or staging</td></tr><tr><td>Connection refused</td><td>Nothing listening on that port, or port not published</td><td>Check the process, <code>docker ps</code>, <code>-p</code></td></tr><tr><td>Connection reset or empty reply from a container</td><td>Server bound to <code>127.0.0.1</code> inside the container</td><td>Bind to <code>0.0.0.0</code></td></tr><tr><td>Works from the host, not from a container</td><td><code>localhost</code> means the container</td><td><code>host.docker.internal</code> or service name</td></tr><tr><td>Certificate error on <code>https://localhost</code></td><td>Self-signed or local CA certificate</td><td>Add the CA in Settings → Certificates, or turn off verification for that request</td></tr><tr><td>Tunnel URL returns 403 or &quot;host not allowed&quot;</td><td>Dev server checks the <code>Host</code> header</td><td>Add the tunnel host to <code>allowedHosts</code></td></tr></tbody></table></div>
<h2 id="which-option-should-you-use">Which option should you use?</h2>
<ul><li><strong>You like working in the browser</strong>: the local bridge. One command, and HTTP requests and collection runs from the web app reach your machine.</li><li><strong>Daily development on Windows or Linux, every protocol</strong>: the desktop app. No setup, and your collection works unchanged against staging later.</li><li><strong>You need the web app, a teammate, or an incoming webhook</strong>: a tunnel, for as long as the test lasts.</li><li><strong>Pre-release checks, monitors and shared testing</strong>: a staging deployment.</li></ul>
<p>Whichever you choose, keep the address in an environment variable so the same requests, scripts and <a href="https://swastik.codes/features/collection-runner/">collection runs</a> work everywhere. When your local API is ready to be tested properly, the guide on <a href="https://swastik.codes/blog/how-to-test-rest-api-online/">how to test a REST API online</a> covers the next steps.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="can-i-test-a-localhost-api-online-without-installing-anything">Can I test a localhost API online without installing anything?</h3>
<p>Almost. An online client that sends from its own server cannot see your machine's <code>localhost</code> by itself. With Swastik Codes, the only thing to install is the single-file <code>swc</code> tool: run <code>swc bridge</code> and switch on <strong>Local bridge</strong> in the web app. Without anything installed, the route is a tunnel that gives your local port a public HTTPS URL.</p>
<h3 id="why-does-swastik-codes-say-refusing-to-connect-for-localhost">Why does Swastik Codes say &quot;refusing to connect&quot; for localhost?</h3>
<p>The web app sends requests through the swastik.codes server, and that server refuses private, loopback and link-local addresses so it cannot be used to reach internal networks. Switch on the local bridge (<code>swc bridge</code>) or use the <a href="https://swastik.codes/download/">desktop app</a> to send from your own computer instead.</p>
<h3 id="do-i-need-to-enable-cors-on-my-local-api-to-test-it">Do I need to enable CORS on my local API to test it?</h3>
<p>Not for the local bridge or a desktop client. CORS is enforced by browsers on web pages, so the bridge, the desktop app, <code>curl</code> and other native clients ignore it. You do need CORS if your own browser frontend, served from a different origin, calls the API.</p>
<h3 id="what-is-the-difference-between-0-0-0-0-and-127-0-0-1">What is the difference between 0.0.0.0 and 127.0.0.1?</h3>
<p>As a bind address, <code>127.0.0.1</code> accepts connections only from the same machine (or the same container), while <code>0.0.0.0</code> accepts them on every network interface. Inside Docker, bind to <code>0.0.0.0</code> so published ports reach your server.</p>
<h3 id="is-there-a-swastik-codes-desktop-app-for-mac">Is there a Swastik Codes desktop app for Mac?</h3>
<p>Not yet. The desktop app is available for Windows and Linux. On a Mac, use the web app with a tunnel or a staging server for now.</p>
<h3 id="can-i-test-a-websocket-or-grpc-server-on-localhost">Can I test a WebSocket or gRPC server on localhost?</h3>
<p>Yes, with the desktop app: WebSocket, Socket.IO, SSE, gRPC, MQTT and MCP connections all start on your machine. See <a href="https://swastik.codes/blog/test-websocket-api-online/">how to test a WebSocket API</a> and the <a href="https://swastik.codes/blog/grpc-client-in-the-browser/">gRPC client guide</a> for the protocol details.</p>]]></content:encoded>
</item>
<item>
<title>How to test a REST API online, step by step</title>
<link>https://swastik.codes/blog/how-to-test-rest-api-online/</link>
<guid isPermaLink="true">https://swastik.codes/blog/how-to-test-rest-api-online/</guid>
<pubDate>Fri, 23 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>REST</category><category>API testing</category><category>Tutorial</category><category>Beginners</category>
<description>Test a REST API from your browser: pick a method, add params, headers, a JSON body and auth, read the response, save it, add a test and run it.</description>
<content:encoded><![CDATA[<p>To test a REST API online, open a browser-based API client, enter the endpoint URL, choose the HTTP method, add any query parameters, headers, body and credentials, and send the request. Then check the status code, response time, headers and body against what the API is supposed to return, and turn those checks into a saved test so you can repeat them. In Swastik Codes the whole loop happens in one browser tab, and the steps below take you from a first GET request to a small test suite you can run in one click.</p>
<h2 id="what-you-need-before-you-start">What you need before you start</h2>
<ul><li><strong>An endpoint that is reachable from the public internet.</strong> The examples use <code>https://jsonplaceholder.typicode.com</code>, a free fake REST API, and <code>https://httpbin.org</code>, which echoes back what you send. Both can be called right away.</li><li><strong>A free Swastik Codes account.</strong> The web app needs an <a href="https://swastik.codes/signup">account</a>; sign up with GitHub or email. There are no paid plans.</li><li><strong>The API's documentation</strong>, or at least the URL, the expected method and how it authenticates.</li></ul>
<h3 id="why-the-api-has-to-be-public">Why the API has to be public</h3>
<p>A web page cannot send arbitrary cross-origin requests with any header it likes: the browser's CORS rules stop it. So the web app does not call your API directly. It sends the request to the swastik.codes server, which makes the real HTTP call and returns the response. To protect itself and other users, that server refuses private, loopback and link-local addresses, so <code>http://localhost:3000</code> or <code>http://192.168.1.10</code> will not work from the web app. For an API on your own machine, use the <a href="https://swastik.codes/download/">desktop app</a> for Windows and Linux, which sends requests from your computer. The <a href="https://swastik.codes/blog/test-localhost-api/">localhost testing guide</a> covers that case in detail.</p>
<h2 id="step-1-send-your-first-get-request">Step 1: send your first GET request</h2>
<ol><li>Open <a href="https://swastik.codes/app">the app</a> and sign in.</li><li>Click <strong>+</strong> at the end of the tab bar and choose <strong>HTTP</strong>. A new request tab opens with the method set to GET.</li><li>Paste <code>https://jsonplaceholder.typicode.com/users/1</code> into the URL bar.</li><li>Press <strong>Send</strong>, or <code>Ctrl+Enter</code>.</li></ol>
<p>The response panel shows <code>200 OK</code>, the time the request took, the size of the response and a JSON object describing a user. That is a complete REST API test in its simplest form: you asked for a resource and the server returned it.</p>
<h2 id="step-2-choose-the-right-http-method">Step 2: choose the right HTTP method</h2>
<p>REST APIs use the HTTP method to say what you want to do with a resource. The method picker lists GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS, and you can type a custom verb if your API uses one.</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Method</th><th>Typical use</th><th>Has a request body?</th><th>Safe</th><th>Idempotent</th></tr></thead><tbody><tr><td>GET</td><td>Read a resource or a list</td><td>No (not meaningful)</td><td>Yes</td><td>Yes</td></tr><tr><td>HEAD</td><td>Like GET, headers only</td><td>No</td><td>Yes</td><td>Yes</td></tr><tr><td>POST</td><td>Create a resource, or run an action</td><td>Usually</td><td>No</td><td>No</td></tr><tr><td>PUT</td><td>Replace a resource completely</td><td>Yes</td><td>No</td><td>Yes</td></tr><tr><td>PATCH</td><td>Change part of a resource</td><td>Yes</td><td>No</td><td>No (in general)</td></tr><tr><td>DELETE</td><td>Remove a resource</td><td>Rarely</td><td>No</td><td>Yes</td></tr><tr><td>OPTIONS</td><td>Ask what the server allows</td><td>No</td><td>Yes</td><td>Yes</td></tr></tbody></table></div>
<p>&quot;Safe&quot; and &quot;idempotent&quot; are defined in RFC 9110: a safe method does not change state on the server, and an idempotent one has the same effect whether you send it once or several times. This matters when testing. Repeating a GET or a PUT should be harmless; repeating a POST usually creates a second resource.</p>
<h2 id="step-3-add-query-parameters-and-path-variables">Step 3: add query parameters and path variables</h2>
<p>Query parameters filter or page a list. Change the URL to <code>https://jsonplaceholder.typicode.com/posts</code> and open the <strong>Params</strong> tab. Add a row with the key <code>userId</code> and the value <code>1</code>. The URL updates to <code>.../posts?userId=1</code> as you type, and editing the URL updates the table: the two stay in sync. Untick a row to leave it out without deleting it.</p>
<p>Path variables are written with a colon. A URL such as <code>https://jsonplaceholder.typicode.com/posts/:id</code> shows an <code>id</code> field in the Params tab where you fill in the value. This keeps the request readable when you save it.</p>
<h2 id="step-4-set-headers">Step 4: set headers</h2>
<p>Open the <strong>Headers</strong> tab to add request headers. The ones you will add most often:</p>
<ul><li><code>Accept: application/json</code> to ask for JSON.</li><li><code>Authorization</code> for credentials (the Authorization tab can build it for you, see step 6).</li><li>Custom headers your API requires, such as <code>X-Api-Version</code> or <code>X-Request-Id</code>.</li></ul>
<p>The Headers tab also lists the headers the app generates automatically, such as <code>Content-Type</code> for a JSON body and the <code>User-Agent</code>. Expand that list to see exactly what will be sent and switch off the optional ones.</p>
<h2 id="step-5-send-a-json-body">Step 5: send a JSON body</h2>
<p>To create something you usually POST a JSON body. Create a new request:</p>
<ol><li>Set the method to <strong>POST</strong> and the URL to <code>https://jsonplaceholder.typicode.com/posts</code>.</li><li>Open the <strong>Body</strong> tab, choose <strong>raw</strong> and set the language to <strong>JSON</strong>.</li><li>Enter the body:</li></ol>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{
  &quot;title&quot;: &quot;Testing a REST API online&quot;,
  &quot;body&quot;: &quot;First post from Swastik Codes&quot;,
  &quot;userId&quot;: 1
}</code></pre></div>
<ol start="4"><li>Send it.</li></ol>
<p>JSONPlaceholder answers <code>201 Created</code> with your data plus a new <code>id</code>. (It is a fake API: nothing is stored, so the id will not exist if you request it later. A real API behaves the same way, minus that caveat.) A raw JSON body gets <code>Content-Type: application/json</code> automatically, which is what most APIs need to parse it.</p>
<p>The other body modes cover the remaining cases: <strong>form-data</strong> for file uploads, <strong>x-www-form-urlencoded</strong> for classic HTML-form posts, <strong>binary</strong> to send a whole file, and <strong>GraphQL</strong> for a query plus variables. For the details of each, see the <a href="https://swastik.codes/features/rest-api-client/">REST API client</a> page.</p>
<h2 id="step-6-add-authentication">Step 6: add authentication</h2>
<p>Most real APIs reject anonymous requests with <code>401 Unauthorized</code>. Instead of building the <code>Authorization</code> header by hand, open the <strong>Authorization</strong> tab and pick a type:</p>
<ul><li><strong>Bearer token</strong> for an access token: the app sends <code>Authorization: Bearer &lt;token&gt;</code>.</li><li><strong>API key</strong> for a key sent in a header or a query parameter.</li><li><strong>Basic</strong> for a username and password.</li><li><strong>OAuth 2.0</strong> when you need to obtain a token first; see <a href="https://swastik.codes/blog/oauth2-in-api-client/">OAuth 2.0 in an API client</a>.</li></ul>
<p>Other schemes, including JWT signing, Digest, AWS Signature v4 and NTLM, are listed on the <a href="https://swastik.codes/features/authorization/">authorization</a> page. You can test Basic auth against httpbin: send a GET to <code>https://httpbin.org/basic-auth/demo/secret</code> with Basic auth <code>demo</code> / <code>secret</code> and you get <code>200</code>; change the password and you get <code>401</code>.</p>
<p>Do not paste real secrets straight into requests you plan to share. Put them in an environment variable and reference it as <code>{{token}}</code>; see the next step.</p>
<h2 id="step-7-use-variables-and-environments">Step 7: use variables and environments</h2>
<p>Hard-coded URLs become a problem as soon as you have a staging and a production server. Create an environment (for example <em>Staging</em>) with variables such as:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Variable</th><th>Value</th></tr></thead><tbody><tr><td><code>baseUrl</code></td><td><code>https://jsonplaceholder.typicode.com</code></td></tr><tr><td><code>token</code></td><td>your access token</td></tr></tbody></table></div>
<p>Then write the URL as <code>{{baseUrl}}/users/1</code> and the bearer token as <code>{{token}}</code>. Switching environments switches every request at once, and unresolved variables are marked in red before you send. Each variable has an initial value, which is shared with your team, and a current value, which stays on your device: keep personal tokens in the current value. More on this on the <a href="https://swastik.codes/features/environments-variables/">environments and variables</a> page.</p>
<h2 id="step-8-read-the-response-properly">Step 8: read the response properly</h2>
<p>A passing status code does not mean the API works. Read the whole response:</p>
<ul><li><strong>Status line.</strong> The code and reason, for example <code>200 OK</code> or <code>422 Unprocessable Content</code>. If you are unsure what a code means, the <a href="https://swastik.codes/blog/http-status-codes/">HTTP status codes guide</a> explains the common ones and what to check for each.</li><li><strong>Time and size.</strong> The timing breakdown splits the total into DNS lookup, TCP connect, TLS handshake, time to first byte and download, which tells you whether slowness comes from the network or from the server.</li><li><strong>Body.</strong> View it pretty-printed, raw or as a rendered preview, and search it. Check field names, types and values, not just that something came back.</li><li><strong>Headers.</strong> <code>Content-Type</code>, caching headers, <code>Location</code> after a create, rate-limit headers.</li><li><strong>Cookies.</strong> Cookies the server set, kept in a per-domain cookie jar.</li></ul>
<p>If something looks wrong, open the console panel (<code>Ctrl+Alt+C</code>). It shows the exact request that went out, after variables were resolved: a missing header or a mistyped variable name becomes obvious.</p>
<h2 id="step-9-save-the-request-to-a-collection">Step 9: save the request to a collection</h2>
<p>Requests you only send once are not tests. Press <code>Ctrl+S</code> (or <strong>Save</strong>) and the save dialog asks where to put the request: pick an existing collection or folder, or type a name to create a new collection, and give the request a clear name such as <em>Get user</em> or <em>Create post</em>.</p>
<p>Once your requests live in a collection you can share them with your team, run them all at once, <a href="https://swastik.codes/features/code-generation/">generate code</a> for them, and use them for <a href="https://swastik.codes/features/mock-server/">mock servers</a> and <a href="https://swastik.codes/features/api-documentation/">documentation</a>. Sent requests are also kept in history, so you can recover an unsaved one.</p>
<p>If your requests already exist in Postman, you do not need to rebuild them: <a href="https://swastik.codes/blog/import-postman-collection/">import the Postman collection</a>.</p>
<h2 id="step-10-add-a-test">Step 10: add a test</h2>
<p>A test is a short JavaScript check that runs after the response arrives. Open the saved <em>Get user</em> request, go to the <strong>Scripts</strong> tab and choose <strong>Post-response</strong>. Paste:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('status is 200', function () {
  swc.response.to.have.status(200);
});

swc.test('responds in under a second', function () {
  swc.expect(swc.response.responseTime).to.be.below(1000);
});

swc.test('returns the expected user', function () {
  const user = swc.response.json();
  swc.expect(user.id).to.eql(1);
  swc.expect(user.email).to.include('@');
  swc.expect(user).to.have.property('address');
});</code></pre></div>
<p>Send the request again and open the <strong>Test Results</strong> tab in the response panel, which lists each test by name with a passed/total count. Change <code>eql(1)</code> to <code>eql(2)</code> and send again to see what a failure looks like.</p>
<p>The script editor offers snippets such as <em>Status code is 200</em>, <em>JSON value check</em> and <em>Validate JSON schema</em>, so you rarely start from nothing. Scripts use the <code>swc.*</code> API; Postman's <code>pm.*</code> is the same object, so scripts written for Postman run unchanged. The <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">guide to swc.* test scripts</a> has many more examples.</p>
<p>Add a test to the <em>Create post</em> request too:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('post is created', function () {
  swc.response.to.have.status(201);
  const post = swc.response.json();
  swc.expect(post.id).to.be.a('number');
  swc.expect(post.title).to.eql('Testing a REST API online');
});</code></pre></div>
<h2 id="step-11-run-the-collection">Step 11: run the collection</h2>
<p>With two or more saved requests that have tests, open the collection's menu in the sidebar and choose <strong>Run collection</strong>. The <a href="https://swastik.codes/features/collection-runner/">collection runner</a> lists the requests in order; you can reorder them for this run, pick an environment, set the number of iterations, add a delay between requests and choose when to stop early.</p>
<p>Start the run. Each request is sent in turn, its scripts run, and the summary shows how many requests and tests passed or failed and the average response time. Export the result as JSON, JUnit XML or an HTML report if someone else needs to see it.</p>
<p>From here you can go further:</p>
<ul><li><strong>Pass data between requests</strong>, for example a token from a login call or an id from a create call. See <a href="https://swastik.codes/blog/chain-api-requests/">chaining API requests</a>.</li><li><strong>Run the same requests with many inputs</strong> from a CSV or JSON file: <a href="https://swastik.codes/blog/data-driven-api-testing/">data-driven API testing</a>.</li><li><strong>Run on a schedule</strong> with <a href="https://swastik.codes/features/api-monitoring/">monitors</a>, which alert you by email or webhook when tests fail.</li></ul>
<h2 id="online-api-tester-or-something-else">Online API tester or something else?</h2>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Tool</th><th>Good for</th><th>Limits</th></tr></thead><tbody><tr><td>Online API client (web app)</td><td>Nothing to install, shareable collections, tests and runs from any computer</td><td>Target must be publicly reachable</td></tr><tr><td>Desktop API client</td><td>APIs on localhost or a private network</td><td>Needs installing; Swastik Codes' desktop app is for Windows and Linux</td></tr><tr><td>curl</td><td>Quick one-off calls, scripts, CI</td><td>No saved collections or visual response; tests are up to you</td></tr></tbody></table></div>
<p>They work well together. Swastik Codes can import a curl command into a request and generate a curl snippet from one; see <a href="https://swastik.codes/blog/convert-curl-to-code/">converting curl to code</a>.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="can-i-test-a-rest-api-online-for-free">Can I test a REST API online for free?</h3>
<p>Yes. Swastik Codes is free to use, with no paid plans. You need a free account to send requests, because the web app sends them through the swastik.codes server.</p>
<h3 id="can-an-online-api-tester-call-localhost">Can an online API tester call localhost?</h3>
<p>Not from the Swastik Codes web app: its server refuses private, loopback and link-local addresses. Use the desktop app for Windows and Linux, which sends requests from your own machine, or deploy the API somewhere public.</p>
<h3 id="what-should-i-check-when-testing-a-rest-api">What should I check when testing a REST API?</h3>
<p>The status code, the response time, the headers (especially <code>Content-Type</code>) and the body: field names, types and values. Also test the failure cases: missing auth, invalid input and resources that do not exist should return the right 4xx codes.</p>
<h3 id="do-i-need-to-know-javascript-to-write-api-tests">Do I need to know JavaScript to write API tests?</h3>
<p>Only a little. Most tests are a few lines built from snippets such as <em>Status code is 200</em> and <em>JSON value check</em>, which the script editor inserts for you.</p>
<h3 id="will-my-postman-collections-and-tests-work">Will my Postman collections and tests work?</h3>
<p>Yes, in most cases. You can import Postman collections (v2.0, v2.1 and v3), environments and globals, and scripts using <code>pm.*</code> run unchanged.</p>]]></content:encoded>
</item>
<item>
<title>How to test JWT authentication in an API</title>
<link>https://swastik.codes/blog/test-jwt-authentication/</link>
<guid isPermaLink="true">https://swastik.codes/blog/test-jwt-authentication/</guid>
<pubDate>Fri, 23 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>JWT</category><category>Authentication</category><category>API testing</category><category>Tutorial</category>
<description>Test JWT auth step by step: log in, save the token from a script, send it as Bearer, decode claims, and check expired, tampered and missing tokens.</description>
<content:encoded><![CDATA[<p>To test JWT authentication in an API, call the login endpoint, save the returned token in a variable with a test script, and send it on protected requests as <code>Authorization: Bearer &lt;token&gt;</code>. Then test the failure cases on purpose: no token, an expired token, a token with a broken signature and a token without the right permissions, and check that each gets the status code your API documents (usually 401 or 403). In Swastik Codes the whole suite is a collection: one login request, Bearer auth inherited from the collection, and <code>swc.test</code> assertions on every response.</p>
<p>This guide walks through that collection step by step, with scripts you can paste into the app. It covers custom login endpoints that return a JWT. If your API uses an OAuth 2.0 provider such as Auth0, Okta or Keycloak, the token step is different; see <a href="https://swastik.codes/blog/oauth2-in-api-client/">OAuth 2.0 in an API client</a> and come back here for the testing part.</p>
<h2 id="what-a-jwt-is">What a JWT is</h2>
<p>A JSON Web Token (JWT, <a href="https://www.rfc-editor.org/rfc/rfc7519" rel="noopener">RFC 7519</a>) is a compact, URL-safe string that carries a set of claims. The usual form is a signed token (a JWS) made of three Base64url-encoded parts separated by dots:</p>
<div class="code"><div class="code-head"><span>Text</span></div><pre tabindex="0"><code class="language-text">header.payload.signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c</code></pre></div>
<ul><li>The <strong>header</strong> names the signing algorithm, for example <code>{&quot;alg&quot;:&quot;HS256&quot;,&quot;typ&quot;:&quot;JWT&quot;}</code>.</li><li>The <strong>payload</strong> holds the claims, for example <code>{&quot;sub&quot;:&quot;1234567890&quot;,&quot;name&quot;:&quot;John Doe&quot;,&quot;iat&quot;:1516239022}</code>.</li><li>The <strong>signature</strong> is computed over <code>header.payload</code> with a secret (HMAC algorithms such as HS256) or a private key (RS256, ES256, EdDSA and others).</li></ul>
<p>A signed JWT is not encrypted. Anyone holding it can decode the payload, so tests may read the claims freely, and APIs should never put secrets in them. The server trusts a token only after it verifies the signature and checks the time-based claims.</p>
<p>The registered claims you will meet most often:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Claim</th><th>Meaning</th><th>What to test</th></tr></thead><tbody><tr><td><code>sub</code></td><td>subject, usually the user id</td><td>matches the user who logged in</td></tr><tr><td><code>exp</code></td><td>expiry, in seconds since 1970-01-01 UTC</td><td>in the future, and a sensible distance away</td></tr><tr><td><code>iat</code></td><td>issued-at time</td><td>close to now</td></tr><tr><td><code>nbf</code></td><td>not valid before</td><td>not in the future, unless intended</td></tr><tr><td><code>iss</code></td><td>issuer</td><td>your auth service</td></tr><tr><td><code>aud</code></td><td>audience</td><td>the API that should accept the token</td></tr><tr><td><code>jti</code></td><td>unique token id</td><td>present if your API uses it for revocation</td></tr></tbody></table></div>
<p>Roles, scopes and tenant ids are usually custom claims; test them the same way.</p>
<h2 id="step-1-log-in-and-save-the-token">Step 1: log in and save the token</h2>
<p>Create a collection for the API and a collection variable <code>baseUrl</code> pointing at your server, or define it per environment as described in <a href="https://swastik.codes/features/environments-variables/">environments and variables</a>. Add a request:</p>
<div class="code"><div class="code-head"><span>HTTP</span></div><pre tabindex="0"><code class="language-http">POST {{baseUrl}}/auth/login
Content-Type: application/json

{ &quot;email&quot;: &quot;{{email}}&quot;, &quot;password&quot;: &quot;{{password}}&quot; }</code></pre></div>
<p>Set this request's <strong>Authorization</strong> tab to <strong>No Auth</strong>, because you do not have a token yet. Keep <code>email</code> and <code>password</code> in an environment's current values or in the encrypted vault rather than in the request.</p>
<p>In the request's <strong>Scripts</strong> tab, add a post-response script that checks the reply and stores the token:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('login returns 200', function () {
  swc.response.to.have.status(200);
});

const body = swc.response.json();

swc.test('response contains a JWT', function () {
  swc.expect(body.access_token, 'access_token').to.be.a('string');
  swc.expect(body.access_token.split('.')).to.have.lengthOf(3);
});

swc.environment.set('accessToken', body.access_token);
if (body.refresh_token) {
  swc.environment.set('refreshToken', body.refresh_token);
}</code></pre></div>
<p>Field names differ between APIs (<code>token</code>, <code>accessToken</code>, <code>access_token</code>), so match yours. <code>swc.environment.set</code> needs an active environment; if you are not using one, write to <code>swc.collectionVariables</code> instead. If you already have Postman scripts, the same code works with <code>pm.</code> in place of <code>swc.</code>.</p>
<h2 id="step-2-send-the-token-as-a-bearer-header">Step 2: send the token as a Bearer header</h2>
<p>Open the collection, select its <strong>Authorization</strong> tab, choose <strong>Bearer Token</strong> and enter <code>{{accessToken}}</code> as the token. Every request left on <strong>Inherit auth from parent</strong> now sends:</p>
<div class="code"><div class="code-head"><span>HTTP</span></div><pre tabindex="0"><code class="language-http">Authorization: Bearer eyJhbGciOi...</code></pre></div>
<p>The login request keeps its own <strong>No Auth</strong> setting. Add a protected request such as <code>GET {{baseUrl}}/me</code> with a test:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('authenticated request succeeds', function () {
  swc.response.to.have.status(200);
});

swc.test('returns the logged-in user', function () {
  swc.expect(swc.response.json().email).to.eql(swc.environment.get('email'));
});</code></pre></div>
<p>Send the login request first, then <code>/me</code>. In the <a href="https://swastik.codes/features/collection-runner/">collection runner</a>, keep the login request at the top so every later request has a fresh token. Runs persist variables changed by scripts by default, so the token saved by the first request is used by the rest.</p>
<h2 id="step-3-decode-the-token-and-check-its-claims">Step 3: decode the token and check its claims</h2>
<p>The sandbox provides <code>atob</code> and <code>btoa</code>, which is all you need to read a JWT. Base64url uses <code>-</code> and <code>_</code> instead of <code>+</code> and <code>/</code> and drops the padding, so convert before decoding:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">function decodePart(part) {
  let b64 = part.replace(/-/g, '+').replace(/_/g, '/');
  while (b64.length % 4) b64 += '=';
  return JSON.parse(decodeURIComponent(escape(atob(b64))));
}

const token = swc.response.json().access_token;
const [headerPart, payloadPart] = token.split('.');
const header = decodePart(headerPart);
const claims = decodePart(payloadPart);
const now = Math.floor(Date.now() / 1000);

swc.test('token uses the expected algorithm', function () {
  swc.expect(header.alg).to.be.oneOf(['RS256', 'ES256']);
});

swc.test('token is issued for this API', function () {
  swc.expect(claims.iss).to.eql('https://auth.example.com');
  swc.expect(claims.aud).to.include('orders-api');
});

swc.test('token expires in about 15 minutes', function () {
  swc.expect(claims.exp).to.be.above(now);
  swc.expect(claims.exp - claims.iat).to.be.within(840, 960);
});</code></pre></div>
<p>Add this to the login request's post-response script, after the token is saved. Put the <code>decodePart</code> helper at the top of any script that needs it. <code>aud</code> may be a string or an array, hence <code>to.include</code>.</p>
<p>A note on the <code>alg</code> check: a server that accepts <code>&quot;alg&quot;: &quot;none&quot;</code> or lets the token choose its own algorithm is a well-known vulnerability, so it is worth asserting that the token uses the algorithm you expect and, below, that the API rejects other ones.</p>
<p>For HS256 tokens you can also verify the signature in a test with the bundled <code>crypto-js</code>, if your test environment knows the signing secret:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const CryptoJS = require('crypto-js');
const [h, p, s] = token.split('.');
const expected = CryptoJS.HmacSHA256(h + '.' + p, swc.environment.get('jwtSecret'))
  .toString(CryptoJS.enc.Base64)
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

swc.test('signature verifies with the test secret', function () {
  swc.expect(s).to.eql(expected);
});</code></pre></div>
<h2 id="step-4-test-the-failure-cases">Step 4: test the failure cases</h2>
<p>Most JWT bugs are in the paths that should fail. HTTP has two status codes for them, and APIs mix them up often:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Status</th><th>Meaning (RFC 9110)</th><th>Typical JWT cases</th></tr></thead><tbody><tr><td><strong>401 Unauthorized</strong></td><td>the request has no valid credentials; the response must include a <code>WWW-Authenticate</code> header</td><td>no token, malformed token, bad signature, expired token, wrong issuer or audience</td></tr><tr><td><strong>403 Forbidden</strong></td><td>the server understood the credentials and refuses anyway</td><td>valid token, but the user lacks the role or scope</td></tr></tbody></table></div>
<p>For Bearer tokens, RFC 6750 adds detail: an invalid or expired token gets 401 with <code>WWW-Authenticate: Bearer error=&quot;invalid_token&quot;</code>, and a token without the required scope gets 403 with <code>error=&quot;insufficient_scope&quot;</code>. Your API may document something else; test what it documents. See <a href="https://swastik.codes/blog/http-status-codes/">HTTP status codes</a> for the rest of the list.</p>
<p>Create a folder <strong>Auth failures</strong> and add one request per case. Set each request's auth explicitly so it does not inherit the valid token.</p>
<h3 id="missing-token">Missing token</h3>
<p>Same URL as <code>/me</code>, auth set to <strong>No Auth</strong>:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('missing token is rejected with 401', function () {
  swc.response.to.be.unauthorized;
});

swc.test('401 includes a Bearer challenge', function () {
  swc.response.to.have.header('WWW-Authenticate');
  swc.expect(swc.response.headers.get('WWW-Authenticate')).to.match(/^Bearer/i);
});</code></pre></div>
<h3 id="tampered-payload">Tampered payload</h3>
<p>Take the valid token, change a claim and keep the old signature. A correct server rejects it. Set the request to <strong>No Auth</strong> and add this pre-request script:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">function decodePart(part) {
  let b64 = part.replace(/-/g, '+').replace(/_/g, '/');
  while (b64.length % 4) b64 += '=';
  return JSON.parse(decodeURIComponent(escape(atob(b64))));
}
function encodePart(obj) {
  return btoa(unescape(encodeURIComponent(JSON.stringify(obj))))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

const [h, p, s] = swc.environment.get('accessToken').split('.');
const claims = decodePart(p);
claims.role = 'admin';

swc.request.headers.upsert({
  key: 'Authorization',
  value: 'Bearer ' + h + '.' + encodePart(claims) + '.' + s
});</code></pre></div>
<p>The test is <code>swc.response.to.be.unauthorized</code>. The same pre-request pattern covers the <code>alg: none</code> case: build the header <code>{&quot;alg&quot;:&quot;none&quot;,&quot;typ&quot;:&quot;JWT&quot;}</code>, encode it with <code>encodePart</code>, and send <code>header.payload.</code> with an empty signature. Expect 401.</p>
<h3 id="expired-or-wrongly-signed-token">Expired or wrongly signed token</h3>
<p>You can create test tokens without touching your auth server: the <strong>JWT Bearer</strong> authorization type signs a token from the payload you type and sends it with the request. It supports HS256/384/512, RS256/384/512, PS256/384/512, ES256/384/512 and EdDSA, takes an HMAC secret (optionally Base64-encoded) or a PEM private key, accepts extra header fields such as <code>kid</code>, and can send the token in the <code>Authorization</code> header (prefix <code>Bearer</code> by default) or as a query parameter.</p>
<p>The payload is signed exactly as typed; no claims are added for you. To make an expired token, use an <code>exp</code> in the past:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{
  &quot;sub&quot;: &quot;42&quot;,
  &quot;iss&quot;: &quot;https://auth.example.com&quot;,
  &quot;aud&quot;: &quot;orders-api&quot;,
  &quot;iat&quot;: 1700000000,
  &quot;exp&quot;: 1700000600
}</code></pre></div>
<p>With the real test signing secret this token has a valid signature but expired long ago, so the API should answer 401. Keep the payload and change the secret to any other value, and you have a token with an invalid signature, which should also get 401. Fields accept <code>{{variables}}</code>, including in the payload, so keep the secret in an environment or the vault, not in the request.</p>
<p>Use only a test or staging signing key for this, never a production one.</p>
<h3 id="valid-token-insufficient-permissions">Valid token, insufficient permissions</h3>
<p>Log in as a user without the admin role (a second login request saving <code>userToken</code>), then call an admin endpoint with Bearer <code>{{userToken}}</code>:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('non-admin gets 403, not 401', function () {
  swc.response.to.be.forbidden;
});</code></pre></div>
<p>If this returns 401, the API is treating &quot;not allowed&quot; as &quot;not authenticated&quot;, and clients will wrongly try to log in again.</p>
<h2 id="step-5-test-token-refresh">Step 5: test token refresh</h2>
<p>Short-lived access tokens are usually paired with a refresh token. Test the refresh endpoint as its own request:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// POST {{baseUrl}}/auth/refresh with body { &quot;refresh_token&quot;: &quot;{{refreshToken}}&quot; }
const body = swc.response.json();

swc.test('refresh returns a new access token', function () {
  swc.response.to.have.status(200);
  swc.expect(body.access_token).to.not.eql(swc.environment.get('accessToken'));
});

swc.environment.set('previousRefreshToken', swc.environment.get('refreshToken'));
swc.environment.set('accessToken', body.access_token);
swc.environment.set('refreshToken', body.refresh_token);</code></pre></div>
<p>If your API rotates refresh tokens, add a request that sends <code>{{previousRefreshToken}}</code> again and expects it to be rejected. A reused refresh token being accepted is a real security gap, and it is easy to miss without a test. Also test logout: after it, the refresh token should no longer work.</p>
<p>For long collection runs, a collection-level pre-request script can refresh the token when it is about to expire, with <code>swc.sendRequest</code>:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// paste the decodePart() helper from Step 3 here

const token = swc.environment.get('accessToken');
const exp = token ? decodePart(token.split('.')[1]).exp : 0;

if (exp - 30 &lt; Date.now() / 1000 &amp;&amp; swc.environment.get('refreshToken')) {
  swc.sendRequest({
    url: swc.environment.get('baseUrl') + '/auth/refresh',
    method: 'POST',
    header: { 'Content-Type': 'application/json' },
    body: { mode: 'raw', raw: JSON.stringify({ refresh_token: swc.environment.get('refreshToken') }) }
  }, function (err, res) {
    if (err) { console.log(err); return; }
    swc.environment.set('accessToken', res.json().access_token);
    swc.environment.set('refreshToken', res.json().refresh_token);
  });
}</code></pre></div>
<p>Requests made from scripts show up in the console. More script patterns are in <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">writing API tests with swc.* scripts</a>, and passing values between requests is covered in <a href="https://swastik.codes/blog/chain-api-requests/">chaining API requests</a>.</p>
<h2 id="running-the-suite">Running the suite</h2>
<p>Order the collection as: login, protected requests, the <strong>Auth failures</strong> folder, refresh. Run it in the <a href="https://swastik.codes/features/collection-runner/">collection runner</a> with the environment selected, and export the results as JUnit XML or HTML if you need them in CI or a ticket. To catch a broken auth deployment early, attach the collection to a <a href="https://swastik.codes/features/api-monitoring/">monitor</a> and get an email or webhook alert when a test fails.</p>
<p>Requests from the web app are sent through the swastik.codes server, which needs a free account and refuses private and loopback addresses. An auth service on <code>localhost</code> needs the <a href="https://swastik.codes/download/">desktop app</a> for Windows or Linux; see <a href="https://swastik.codes/blog/test-localhost-api/">testing a localhost API</a>.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="should-an-expired-jwt-return-401-or-403">Should an expired JWT return 401 or 403?</h3>
<ol start="401"><li>The token is no longer valid credentials, so the request is unauthenticated. RFC 6750 specifies 401 with <code>WWW-Authenticate: Bearer error=&quot;invalid_token&quot;</code> for expired tokens. 403 is for a valid token whose user or scopes are not allowed to do the action.</li></ol>
<h3 id="how-do-i-decode-a-jwt-in-a-test-script">How do I decode a JWT in a test script?</h3>
<p>Split the token on <code>.</code>, convert the second part from Base64url to Base64 (replace <code>-</code> with <code>+</code> and <code>_</code> with <code>/</code>, then pad with <code>=</code>), decode it with <code>atob</code> and parse the JSON. The <code>decodePart</code> helper above does this. Decoding does not verify the signature; only the server, or a test that knows the key, can do that.</p>
<h3 id="can-swastik-codes-generate-a-jwt-for-me">Can Swastik Codes generate a JWT for me?</h3>
<p>Yes. The JWT Bearer authorization type signs a token from your payload on every send, with HMAC, RSA, RSA-PSS, ECDSA or EdDSA algorithms, and adds it as a header or query parameter. It is useful for services that accept self-signed tokens and for creating expired or wrongly signed tokens in negative tests.</p>
<h3 id="how-do-i-reuse-one-token-across-all-requests">How do I reuse one token across all requests?</h3>
<p>Save it to a variable in the login request's test script, set the collection's auth to Bearer with <code>{{accessToken}}</code>, and leave the other requests on Inherit auth from parent. Set the login request itself to No Auth.</p>
<h3 id="is-it-safe-to-paste-a-jwt-into-an-api-client">Is it safe to paste a JWT into an API client?</h3>
<p>Treat access tokens like passwords: they grant access until they expire. Keep them in environment current values or the vault, which are not shared with your team, use test accounts, and avoid long-lived tokens.</p>]]></content:encoded>
</item>
<item>
<title>HTTP status codes explained, with API testing tips</title>
<link>https://swastik.codes/blog/http-status-codes/</link>
<guid isPermaLink="true">https://swastik.codes/blog/http-status-codes/</guid>
<pubDate>Fri, 23 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>HTTP</category><category>REST</category><category>API testing</category><category>Reference</category>
<description>What the common HTTP status codes from 1xx to 5xx mean under RFC 9110, what to check for each when testing an API, and how to assert them in tests.</description>
<content:encoded><![CDATA[<p>An HTTP status code is the three-digit number at the start of every response that tells the client how the server handled the request. The first digit gives the class: 1xx informational, 2xx success, 3xx redirection, 4xx client error and 5xx server error. RFC 9110 defines their meaning; for example <code>200 OK</code> means the request succeeded, <code>404 Not Found</code> means the target resource does not exist, and <code>503 Service Unavailable</code> means the server cannot handle the request right now. This guide covers the codes APIs actually return, what to check when you see each one, and how to assert them in API tests.</p>
<h2 id="how-status-codes-work">How status codes work</h2>
<p>Every HTTP response starts with a status code and an optional reason phrase, such as <code>HTTP/1.1 201 Created</code>. The code is for software; the reason phrase is for humans and is not even sent in HTTP/2 and HTTP/3. RFC 9110 says clients should ignore the phrase, so tests and client code should act on the number only.</p>
<p>RFC 9110, section 15, defines the five classes and most of the codes. One rule there is useful to know when testing: a client that does not recognise a code must treat it as the <code>x00</code> code of its class. An unknown <code>499</code> is handled like <code>400</code>, an unknown <code>599</code> like <code>500</code>. That is why tests that check the class (any 2xx, any 4xx) are often more robust than tests that pin one exact code, and why APIs should not invent codes when a standard one fits.</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Class</th><th>Meaning</th><th>Who should act</th></tr></thead><tbody><tr><td>1xx</td><td>Informational, interim response</td><td>The HTTP library, usually invisibly</td></tr><tr><td>2xx</td><td>The request was received, understood and accepted</td><td>Nobody: success</td></tr><tr><td>3xx</td><td>Further action is needed, usually a redirect</td><td>The client, often automatically</td></tr><tr><td>4xx</td><td>The request has an error</td><td>The client must change the request</td></tr><tr><td>5xx</td><td>The server failed to fulfil a valid request</td><td>The server operator</td></tr></tbody></table></div>
<p>The 4xx/5xx split matters most when testing. A 4xx says &quot;your request is wrong; retrying it unchanged will not help&quot;. A 5xx says &quot;the server failed&quot;. An API that returns <code>500</code> for invalid input makes clients guess.</p>
<h2 id="1xx-informational">1xx informational</h2>
<p>You rarely see these in an API client, because the HTTP library handles them before the final response arrives.</p>
<ul><li><strong>100 Continue</strong>: the server is willing to receive the body of a request that was sent with <code>Expect: 100-continue</code>. Used for large uploads.</li><li><strong>101 Switching Protocols</strong>: the server agrees to switch protocols, most often the WebSocket upgrade. If you are testing WebSockets, see <a href="https://swastik.codes/blog/test-websocket-api-online/">testing a WebSocket API online</a>.</li></ul>
<h2 id="2xx-success">2xx success</h2>
<h3 id="200-ok">200 OK</h3>
<p>The request succeeded and the body carries the result. <strong>When testing:</strong> do not stop at the status. Check <code>Content-Type</code>, the body shape and the values; a <code>200</code> with an error object inside is a common bug.</p>
<h3 id="201-created">201 Created</h3>
<p>A new resource was created (RFC 9110, section 15.3.2). The server usually identifies it with a <code>Location</code> header, and many APIs also return the created object. <strong>When testing:</strong> assert <code>201</code> rather than any 2xx for create endpoints, check the <code>Location</code> header or returned id, and fetch the new resource to confirm it really exists.</p>
<h3 id="202-accepted">202 Accepted</h3>
<p>The request was accepted for processing, but processing is not finished and may still fail. Common for jobs, exports and webhooks. <strong>When testing:</strong> the body should tell you how to follow up, typically a job id or a status URL. Poll it until the job finishes; the collection runner's <code>setNextRequest</code> is made for this (see below).</p>
<h3 id="204-no-content">204 No Content</h3>
<p>Success, with no body. Typical for DELETE and for PUT or PATCH when the API does not echo the resource. <strong>When testing:</strong> do not call <code>swc.response.json()</code> on a 204, because there is nothing to parse. Assert the status and, if relevant, fetch the resource afterwards.</p>
<h2 id="3xx-redirection">3xx redirection</h2>
<h3 id="301-moved-permanently-and-308-permanent-redirect">301 Moved Permanently and 308 Permanent Redirect</h3>
<p>The resource has a new permanent URL, given in <code>Location</code>. The difference: on a <code>301</code>, clients historically changed a POST into a GET when following it; a <code>308</code> (section 15.4.9) forbids changing the method.</p>
<h3 id="302-found-and-307-temporary-redirect">302 Found and 307 Temporary Redirect</h3>
<p>The same pair for temporary moves. <code>302</code> allows the historical POST-to-GET change; <code>307</code> requires the client to repeat the same method and body.</p>
<h3 id="303-see-other">303 See Other</h3>
<p>Points the client to another resource that it should fetch with GET. Often used after a POST to send the client to the result.</p>
<h3 id="304-not-modified">304 Not Modified</h3>
<p>Returned to a conditional GET (with <code>If-None-Match</code> or <code>If-Modified-Since</code>) when the client's cached copy is still valid. It has no body. <strong>When testing caching:</strong> send the request once, copy the <code>ETag</code>, send it again with <code>If-None-Match: &quot;&lt;etag&gt;&quot;</code> and expect <code>304</code>.</p>
<p><strong>Testing redirects in Swastik Codes.</strong> By default the client follows redirects, so you see the final response. To test the redirect itself, open the request's <strong>Settings</strong> tab and turn off <em>Automatically follow redirects</em>; you then see the 3xx and its <code>Location</code> header. The same tab has <em>Follow original HTTP method</em> and a maximum number of redirects. Try <code>https://httpbin.org/redirect-to?url=https://httpbin.org/get&amp;status_code=307</code> with following on and off.</p>
<h2 id="4xx-client-errors">4xx client errors</h2>
<h3 id="400-bad-request">400 Bad Request</h3>
<p>The server cannot or will not process the request because of something it sees as a client error: malformed JSON, a missing required field, an invalid parameter. <strong>When testing:</strong> send deliberately broken input and check that the error body says what is wrong. A <code>400</code> with no explanation is hard for client developers to use.</p>
<h3 id="401-unauthorized">401 Unauthorized</h3>
<p>The request lacks valid authentication credentials. Despite the name, this is about authentication. RFC 9110 requires a <code>401</code> response to include a <code>WWW-Authenticate</code> header that tells the client how to authenticate. <strong>When testing:</strong> call each protected endpoint with no credentials, an invalid token and an expired token, and expect <code>401</code> each time. The <a href="https://swastik.codes/blog/test-jwt-authentication/">JWT authentication testing guide</a> walks through those cases.</p>
<h3 id="403-forbidden">403 Forbidden</h3>
<p>The server understood the request and refuses to fulfil it: this user may not do this. <strong>When testing:</strong> use a valid token for a user without the right role and expect <code>403</code>. Some APIs answer <code>404</code> instead to hide that a resource exists; that is allowed, but should be consistent.</p>
<h3 id="404-not-found">404 Not Found</h3>
<p>The server did not find anything at the target URL, or will not say whether it exists. <strong>When testing:</strong> request an id that does not exist and expect <code>404</code>; a <code>500</code> here means the API crashes on unknown ids. Also check that a typo in the path gives <code>404</code> and not a misleading <code>200</code> from a catch-all route.</p>
<h3 id="405-method-not-allowed">405 Method Not Allowed</h3>
<p>The resource exists but does not support the method. The server must include an <code>Allow</code> header listing the methods it does support (section 15.5.6). <strong>When testing:</strong> send DELETE to a read-only endpoint and check both the status and <code>Allow</code>.</p>
<h3 id="406-not-acceptable-and-415-unsupported-media-type">406 Not Acceptable and 415 Unsupported Media Type</h3>
<p><code>406</code> means the server cannot produce a response in any format listed in your <code>Accept</code> header. <code>415</code> means it will not accept the format of the body you sent, usually a wrong or missing <code>Content-Type</code>. <strong>When testing:</strong> send XML to a JSON-only endpoint and expect <code>415</code>.</p>
<h3 id="409-conflict">409 Conflict</h3>
<p>The request conflicts with the current state of the resource: a duplicate username, an edit based on an outdated version. <strong>When testing:</strong> create the same unique resource twice and expect <code>409</code> the second time.</p>
<h3 id="412-precondition-failed">412 Precondition Failed</h3>
<p>A condition in the request headers, such as <code>If-Match: &quot;&lt;etag&gt;&quot;</code>, was false. This is how APIs implement optimistic concurrency. <strong>When testing:</strong> update a resource with a stale <code>ETag</code> and expect <code>412</code>.</p>
<h3 id="422-unprocessable-content">422 Unprocessable Content</h3>
<p>The server understands the content type and the syntax is correct, but it cannot process the instructions: a validation error such as an end date before a start date. Many APIs use <code>422</code> for semantic validation errors and <code>400</code> for malformed requests; others use <code>400</code> for both. Either is defensible; mixing them at random is not.</p>
<h3 id="429-too-many-requests">429 Too Many Requests</h3>
<p>Defined in RFC 6585, section 4. The client has sent too many requests in a given time. The response may include a <code>Retry-After</code> header saying how long to wait. <strong>When testing:</strong> check that rate-limited responses carry <code>Retry-After</code> (or the API's documented rate-limit headers) and that your client backs off. In a collection run, a delay between requests keeps you under the limit.</p>
<h2 id="5xx-server-errors">5xx server errors</h2>
<h3 id="500-internal-server-error">500 Internal Server Error</h3>
<p>A generic server failure. <strong>When testing:</strong> a <code>500</code> is always worth a bug report; also check the body does not leak stack traces or internal hostnames.</p>
<h3 id="502-bad-gateway">502 Bad Gateway</h3>
<p>A gateway or proxy got an invalid response from the server behind it. The API process may have crashed or be restarting.</p>
<h3 id="503-service-unavailable">503 Service Unavailable</h3>
<p>The server is temporarily unable to handle the request, because of overload or maintenance. It may send <code>Retry-After</code>. <strong>When testing:</strong> this is the code a well-behaved API returns under load shedding; <a href="https://swastik.codes/features/load-testing/">load testing</a> is how you find out when it starts.</p>
<h3 id="504-gateway-timeout">504 Gateway Timeout</h3>
<p>A gateway or proxy did not get a response from the upstream server in time. Usually a slow query or a downstream dependency that hangs.</p>
<h2 id="asserting-status-codes-in-tests">Asserting status codes in tests</h2>
<p>In Swastik Codes, post-response scripts use the <code>swc.*</code> API, which is the same object as Postman's <code>pm.*</code>. The most direct check:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('status is 201', function () {
  swc.response.to.have.status(201);
});</code></pre></div>
<p>Readable shortcuts exist for common codes and classes:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('request succeeded (any 2xx)', function () {
  swc.response.to.be.success;
});

swc.test('unknown id gives 404', function () {
  swc.response.to.be.notFound;
});</code></pre></div>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Assertion</th><th>Passes when the status is</th></tr></thead><tbody><tr><td><code>swc.response.to.be.ok</code></td><td>exactly 200</td></tr><tr><td><code>swc.response.to.be.success</code></td><td>200–299</td></tr><tr><td><code>swc.response.to.be.redirection</code></td><td>300–399</td></tr><tr><td><code>swc.response.to.be.clientError</code></td><td>400–499</td></tr><tr><td><code>swc.response.to.be.serverError</code></td><td>500–599</td></tr><tr><td><code>swc.response.to.be.error</code></td><td>400–599</td></tr><tr><td><code>swc.response.to.be.accepted</code></td><td>202</td></tr><tr><td><code>swc.response.to.be.withoutContent</code></td><td>204</td></tr><tr><td><code>swc.response.to.be.badRequest</code></td><td>400</td></tr><tr><td><code>swc.response.to.be.unauthorized</code></td><td>401</td></tr><tr><td><code>swc.response.to.be.forbidden</code></td><td>403</td></tr><tr><td><code>swc.response.to.be.notFound</code></td><td>404</td></tr><tr><td><code>swc.response.to.be.rateLimited</code></td><td>429</td></tr></tbody></table></div>
<p>When more than one code is acceptable, compare the number directly:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('upsert returns 200 or 201', function () {
  swc.expect(swc.response.code).to.be.oneOf([200, 201]);
});</code></pre></div>
<p>Check headers that belong with the code, too:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">if (swc.response.code === 429) {
  swc.test('rate limit response says when to retry', function () {
    swc.response.to.have.header('Retry-After');
  });
}

if (swc.response.code === 201) {
  swc.test('created resource has a Location', function () {
    swc.response.to.have.header('Location');
  });
}</code></pre></div>
<p>Put the status test first in each script. If the body is an HTML error page, <code>swc.response.json()</code> throws, and a failing status test tells you why far more clearly than a JSON parse error. The <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">swc.* scripts guide</a> covers the rest of the assertion API.</p>
<h3 id="testing-negative-cases-with-a-data-file">Testing negative cases with a data file</h3>
<p>Status code tests become much more useful when they cover the failure cases. Put the inputs and expected codes in a CSV file:</p>
<div class="code"><div class="code-head"><span>CSV</span></div><pre tabindex="0"><code class="language-csv">userId,expectedStatus
1,200
99999,404
abc,404</code></pre></div>
<p>Then request <code>https://jsonplaceholder.typicode.com/users/{{userId}}</code> and assert against the row:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('status matches the data row', function () {
  swc.response.to.have.status(Number(swc.iterationData.get('expectedStatus')));
});</code></pre></div>
<p>Attach the file in the <a href="https://swastik.codes/features/collection-runner/">collection runner</a> and each row becomes one iteration. <a href="https://swastik.codes/blog/data-driven-api-testing/">Data-driven API testing</a> explains the setup.</p>
<h3 id="polling-a-202-job">Polling a 202 job</h3>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// Post-response script of &quot;Check job status&quot;
const job = swc.response.json();
if (job.status === 'done') {
  swc.execution.setNextRequest('Download result');
} else {
  swc.execution.setNextRequest('Check job status');
}</code></pre></div>
<p>In a collection run this repeats the status check until the job is done. Add a delay between requests in the runner settings so you do not hammer the API.</p>
<p>To try the assertions, send a GET to <code>https://httpbin.org/status/&lt;code&gt;</code> in <a href="https://swastik.codes/app">the app</a>: it returns whatever status you ask for, such as <code>429</code> or <code>503</code>.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="what-is-the-difference-between-401-and-403">What is the difference between 401 and 403?</h3>
<p><code>401 Unauthorized</code> means the request has no valid authentication; sending correct credentials may fix it, and the response must include <code>WWW-Authenticate</code>. <code>403 Forbidden</code> means the server knows who you are, or does not need to, and still refuses; re-authenticating as the same user will not help.</p>
<h3 id="should-an-api-return-400-or-422-for-validation-errors">Should an API return 400 or 422 for validation errors?</h3>
<p>Both are acceptable under RFC 9110. A common convention is <code>400</code> for malformed requests (invalid JSON, wrong types) and <code>422</code> for well-formed requests that fail business validation. Pick one convention, document it and test it.</p>
<h3 id="is-it-wrong-to-return-200-with-an-error-in-the-body">Is it wrong to return 200 with an error in the body?</h3>
<p>It works, but it hides failures from every tool that looks at status codes: caches, proxies, monitoring and test runners. Use a 4xx or 5xx code and put the details in the body.</p>
<h3 id="what-is-the-difference-between-301-302-307-and-308">What is the difference between 301, 302, 307 and 308?</h3>
<p><code>301</code> and <code>308</code> are permanent, <code>302</code> and <code>307</code> temporary. <code>307</code> and <code>308</code> require the client to keep the original method and body; <code>301</code> and <code>302</code> historically let clients change POST to GET.</p>
<h3 id="when-should-an-api-use-404-instead-of-403">When should an API use 404 instead of 403?</h3>
<p>When revealing that a resource exists would itself leak information, RFC 9110 allows a server to answer <code>404</code> instead of <code>403</code>. Whichever you choose, apply it consistently.</p>
<h3 id="how-do-i-test-that-an-api-returns-the-right-status-code">How do I test that an API returns the right status code?</h3>
<p>Send the request and assert the code in a post-response test, for example <code>swc.response.to.have.status(404)</code>. Cover the error paths as well as the happy path, and run them together in a collection run.</p>]]></content:encoded>
</item>
<item>
<title>Import an OpenAPI spec and generate API tests</title>
<link>https://swastik.codes/blog/test-openapi-spec/</link>
<guid isPermaLink="true">https://swastik.codes/blog/test-openapi-spec/</guid>
<pubDate>Fri, 23 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>OpenAPI</category><category>Swagger</category><category>API testing</category><category>Contract testing</category><category>Tutorial</category>
<description>Import an OpenAPI 3 or Swagger 2 file, generate a collection, add contract tests that validate responses against the spec, and keep both in sync.</description>
<content:encoded><![CDATA[<p>To test an API from its OpenAPI spec, import the OpenAPI 3 or Swagger 2 file into an API client, which turns every operation into a ready-to-send request, point the base URL at your server, and add tests that check each response against the status codes and schemas the spec documents. In Swastik Codes you can import the file as a collection in one step, or add it under <strong>APIs</strong> and generate contract tests that validate status, <code>Content-Type</code> and the JSON body schema for every operation, then run them in the collection runner.</p>
<p>This guide covers both routes, explains what the generated collection contains, and shows how to keep the collection in step with the spec as it changes.</p>
<h2 id="two-ways-to-bring-in-a-spec">Two ways to bring in a spec</h2>
<div class="table-wrap" tabindex="0"><table><thead><tr><th></th><th>Import dialog</th><th>APIs panel</th></tr></thead><tbody><tr><td>Result</td><td>a collection</td><td>a spec you can edit, plus a linked collection</td></tr><tr><td>Linting</td><td>no</td><td>yes, with file and line numbers</td></tr><tr><td>Contract tests from the spec</td><td>no</td><td>yes</td></tr><tr><td>Sync when the spec changes</td><td>re-import</td><td><strong>Update collection</strong> merges changes</td></tr><tr><td>Best for</td><td>a quick look at a third-party API</td><td>an API you own or test regularly</td></tr></tbody></table></div>
<p>Both accept OpenAPI 3.0 and 3.1 and Swagger 2.0, in JSON or YAML.</p>
<h3 id="import-as-a-collection">Import as a collection</h3>
<p>Open <strong>Import</strong> in <a href="https://swastik.codes/app">the app</a> and choose <strong>File</strong>, <strong>Folder</strong>, <strong>Link</strong> or <strong>Raw</strong> (paste the YAML or JSON). A link is fetched by your browser, so it must allow cross-origin requests; if it does not, download the file and use <strong>File</strong> instead. A preview lists what will be created, and the import settings for OpenAPI files let you choose:</p>
<ul><li><strong>Naming requests</strong>: by summary or <code>operationId</code>, or by URL.</li><li><strong>Folder organization</strong>: by tag, by path segment, or no folders.</li><li><strong>Parameter generation</strong>: values from the spec's examples, or type placeholders such as <code>&lt;string&gt;</code> from the schema.</li><li>Whether optional parameters are switched on, whether deprecated operations are included, and whether OpenAPI 3.1 webhooks become requests.</li></ul>
<h3 id="add-it-to-apis">Add it to APIs</h3>
<p>Open <strong>APIs</strong> in the sidebar and use <strong>Import a definition file</strong>, or create a new API and paste the definition. The spec then gets its own view with <strong>Definition</strong>, <strong>Documentation</strong>, <strong>Collection sync</strong>, <strong>Contract tests</strong> and <strong>Versions</strong> tabs. The rest of this guide mostly uses this route, because it keeps the spec and the tests connected. See <a href="https://swastik.codes/features/api-design/">API design</a> for the editor itself.</p>
<h2 id="what-the-generated-collection-contains">What the generated collection contains</h2>
<p>Generation follows the same rules on both routes:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>In the spec</th><th>In the collection</th></tr></thead><tbody><tr><td>each operation (<code>GET /orders/{id}</code>)</td><td>one request, named from <code>summary</code>, <code>operationId</code> or the URL</td></tr><tr><td>tags (or path segments)</td><td>folders</td></tr><tr><td>first entry of <code>servers</code> (or <code>host</code> + <code>basePath</code> + first scheme in Swagger 2)</td><td>the <code>baseUrl</code> collection variable, with server variables filled from their defaults</td></tr><tr><td>path templates <code>{id}</code></td><td>path variables <code>:id</code>, with example values</td></tr><tr><td>query and header parameters</td><td>query and header rows; optional ones can be added switched off</td></tr><tr><td>request bodies</td><td>example bodies built from <code>example</code>/<code>examples</code> or from the schema</td></tr><tr><td>documented responses</td><td>saved examples with status, headers and an example body</td></tr><tr><td><code>securitySchemes</code></td><td>collection or request auth: Bearer with <code>{{bearerToken}}</code>, API key with <code>{{apiKey}}</code>, Basic, Digest and OAuth 2.0</td></tr></tbody></table></div>
<p>Every URL starts with <code>{{baseUrl}}</code>, so changing one variable moves the whole collection to another server. Path and operation-level <code>servers</code> that differ from the global one are kept on those requests. If the spec has no <code>servers</code>, <code>baseUrl</code> is empty and you must set it before sending.</p>
<h2 id="step-1-set-base-urls-with-environments">Step 1: set base URLs with environments</h2>
<p>The generated <code>baseUrl</code> is a collection variable. Create an environment per server and define <code>baseUrl</code> in each:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Environment</th><th><code>baseUrl</code></th></tr></thead><tbody><tr><td>Staging</td><td><code>https://staging-api.example.com/v1</code></td></tr><tr><td>Production</td><td><code>https://api.example.com/v1</code></td></tr></tbody></table></div>
<p>Environment variables take precedence over collection variables, so selecting an environment switches every request at once. Put the credentials the auth expects (<code>bearerToken</code>, <code>apiKey</code>) in the same environments, as current values or in the vault, so they stay on your device. <a href="https://swastik.codes/features/environments-variables/">Environments and variables</a> explains scopes in detail.</p>
<p>Requests from the web app are sent through the swastik.codes server, which needs a free account and does not connect to private or loopback addresses. To test a server running on your own machine, use the <a href="https://swastik.codes/download/">desktop app</a> for Windows or Linux; see <a href="https://swastik.codes/blog/test-localhost-api/">testing a localhost API</a>.</p>
<h2 id="step-2-generate-contract-tests-from-the-spec">Step 2: generate contract tests from the spec</h2>
<p>Open the spec's <strong>Contract tests</strong> tab. It lists the operations and how many of them have a response schema, and offers two actions:</p>
<ul><li><strong>Add to &quot;your collection&quot;</strong> (or <strong>Generate collection with tests</strong> if none is linked yet) writes tests into the requests of the linked collection.</li><li><strong>Create test suite collection</strong> builds a separate collection from the spec that holds only the generated tests, and <strong>Update test suite</strong> refreshes it later.</li></ul>
<p>For each request matched to an operation by method and path, the generated script checks three things:</p>
<ol><li><strong>The status code is documented.</strong> A <code>404</code> passes only if the operation lists <code>404</code>, a matching range such as <code>4XX</code>, or <code>default</code>.</li><li><strong>The <code>Content-Type</code> is documented</strong> for that response (skipped for <code>204</code>).</li><li><strong>The JSON body matches the documented schema</strong>, using <code>swc.response.to.have.jsonSchema</code>.</li></ol>
<p>A generated block looks like this (shortened):</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// &lt;contract-tests&gt; generated from &quot;Orders API&quot; for GET /orders/{id}. Regenerating replaces this block.
(function () {
  var contract = { &quot;200&quot;: { &quot;contentTypes&quot;: [&quot;application/json&quot;], &quot;schema&quot;: { &quot;type&quot;: &quot;object&quot;, &quot;required&quot;: [&quot;id&quot;, &quot;status&quot;], &quot;properties&quot;: { ... } } },
                   &quot;404&quot;: { &quot;contentTypes&quot;: [&quot;application/problem+json&quot;] } };
  var code = String(swc.response.code);
  ...
  swc.test('Response body matches the ' + key + ' schema', function () {
    swc.response.to.have.jsonSchema(expected.schema);
  });
})();
// &lt;/contract-tests&gt;</code></pre></div>
<p>The block sits between <code>// &lt;contract-tests&gt;</code> markers in the request's post-response script. Regenerating replaces only that block, so tests you wrote yourself above or below it are kept. A report after each run of the generator lists requests with no matching operation and operations that no request covers, which is a quick way to find untested endpoints.</p>
<h3 id="how-spec-schemas-become-test-schemas">How spec schemas become test schemas</h3>
<p>OpenAPI schemas are close to JSON Schema but not identical, so the generator converts them:</p>
<ul><li>local <code>$ref</code>s are inlined (a recursive reference becomes an empty schema that accepts anything);</li><li><code>nullable: true</code> (OpenAPI 3.0) becomes a <code>null</code> type, so <code>null</code> values pass;</li><li><code>writeOnly</code> properties are not required in responses;</li><li>OpenAPI 3.0 boolean <code>exclusiveMinimum</code>/<code>exclusiveMaximum</code> become numeric bounds;</li><li><code>format</code> is dropped, so <code>date-time</code> or <code>email</code> formats are not checked;</li><li>schemas larger than 200,000 characters are left out of the script.</li></ul>
<p>If you need format checks, add your own schema test next to the block; <a href="https://swastik.codes/blog/json-schema-validation/">JSON Schema validation in API tests</a> shows how.</p>
<h2 id="step-3-add-your-own-tests">Step 3: add your own tests</h2>
<p>Contract tests answer &quot;does the response match the spec&quot;. They do not know your business rules. Add those to the request's post-response script, outside the markers:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const order = swc.response.json();

swc.test('total equals the sum of the lines', function () {
  const sum = order.lines.reduce((s, l) =&gt; s + l.quantity * l.unitPrice, 0);
  swc.expect(order.total).to.be.closeTo(sum, 0.001);
});

swc.collectionVariables.set('orderId', order.id);</code></pre></div>
<p>Saving ids like this lets later requests use <code>{{orderId}}</code>, so a create → read → update → delete sequence can run end to end. <a href="https://swastik.codes/blog/chain-api-requests/">Chaining API requests</a> covers the pattern, and <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">writing tests with swc.* scripts</a> lists the assertions.</p>
<p>Also test the error paths the spec documents. Duplicate a request, send an invalid body or a missing id, and the contract test checks that the error status and its schema are documented too.</p>
<h2 id="step-4-lint-the-spec">Step 4: lint the spec</h2>
<p>Tests generated from a broken spec give misleading results, so fix the definition first. The <strong>Definition</strong> tab validates on the server as you edit and lists errors and warnings with the file, line, JSON path and rule. Typical findings:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Rule</th><th>Example</th></tr></thead><tbody><tr><td><code>unresolved-ref</code></td><td>a <code>$ref</code> points to a schema that does not exist</td></tr><tr><td><code>path-param-missing</code></td><td><code>/orders/{id}</code> has no <code>id</code> path parameter declared</td></tr><tr><td><code>duplicate-operation-id</code></td><td>two operations share an <code>operationId</code></td></tr><tr><td><code>required-property</code></td><td><code>required</code> lists a property that is not in <code>properties</code></td></tr><tr><td><code>get-request-body</code></td><td>a <code>GET</code> operation declares a request body</td></tr><tr><td><code>no-servers</code></td><td>no <code>servers</code>, so requests will use an empty <code>{{baseUrl}}</code></td></tr><tr><td><code>unused-component</code></td><td>a component is never referenced</td></tr></tbody></table></div>
<p>Click an issue to jump to it. A definition can span several files with <code>$ref</code>s between them; one file is the root, and validation resolves references across files.</p>
<h2 id="step-5-keep-the-collection-and-the-spec-in-sync">Step 5: keep the collection and the spec in sync</h2>
<p>When the spec changes, the header of the spec view shows <strong>Collection out of sync</strong>. Press <strong>Update collection</strong>. In the default <strong>Sync</strong> mode the merge is keyed on <code>METHOD /path</code>:</p>
<ul><li>new operations are added as requests;</li><li>changed operations are updated;</li><li>request ids, test scripts, auth, saved examples and values you edited are kept;</li><li>requests whose operation was removed are kept as orphans, unless you tick <strong>Remove orphan requests</strong>.</li></ul>
<p>A sync report lists what was added, updated, removed or orphaned. The other mode, <strong>Replace</strong>, regenerates every request and loses your edits, so choose it only on purpose. After a sync, run the contract test generator again so the embedded schemas match the new spec; it replaces only the marked blocks.</p>
<p>Tag a version under <strong>Versions</strong> when you release. Each version records a changelog of added, changed and removed operations against the previous one.</p>
<h2 id="step-6-run-the-whole-suite">Step 6: run the whole suite</h2>
<p>From the <strong>Contract tests</strong> tab, <strong>Run test suite</strong> (or <strong>Run</strong> on the linked collection) opens the <a href="https://swastik.codes/features/collection-runner/">collection runner</a>. Pick the environment, set iterations or a data file if needed, and start. The summary shows passed and failed tests per request, and you can export the run as JSON, JUnit XML or a self-contained HTML report. To check the API against its spec every hour or every day, attach the collection to a <a href="https://swastik.codes/features/api-monitoring/">monitor</a>.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="can-i-import-swagger-2-0-as-well-as-openapi-3">Can I import Swagger 2.0 as well as OpenAPI 3?</h3>
<p>Yes. Swagger 2.0, OpenAPI 3.0 and OpenAPI 3.1 are supported, in JSON or YAML. For Swagger 2 the base URL is built from <code>schemes</code>, <code>host</code> and <code>basePath</code>.</p>
<h3 id="does-it-validate-responses-against-the-openapi-schema">Does it validate responses against the OpenAPI schema?</h3>
<p>Yes, through generated contract tests. Each test checks that the status and content type are documented and that the JSON body matches the response schema. Formats are not checked by the generated tests, and request bodies are not validated.</p>
<h3 id="will-updating-the-collection-delete-my-tests">Will updating the collection delete my tests?</h3>
<p>No. Sync mode keeps your scripts, auth, examples and edited values, and the generated contract tests live in a marked block that is the only part regenerated. Only Replace mode rebuilds requests from scratch.</p>
<h3 id="what-if-my-spec-has-no-servers">What if my spec has no servers?</h3>
<p>The <code>baseUrl</code> variable is created empty. Define <code>baseUrl</code> in an environment or on the collection before sending. The linter warns about this with the <code>no-servers</code> rule.</p>
<h3 id="can-i-go-the-other-way-from-a-collection-to-openapi">Can I go the other way, from a collection to OpenAPI?</h3>
<p>Yes. Collections export to OpenAPI 3 in JSON or YAML; see <a href="https://swastik.codes/features/import-export/">import and export</a>.</p>]]></content:encoded>
</item>
<item>
<title>JSON Schema validation in API tests</title>
<link>https://swastik.codes/blog/json-schema-validation/</link>
<guid isPermaLink="true">https://swastik.codes/blog/json-schema-validation/</guid>
<pubDate>Fri, 23 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>JSON Schema</category><category>API testing</category><category>Scripts</category><category>Tutorial</category>
<description>Validate API responses against a JSON Schema in a test script: write the schema, run it with jsonSchema (Ajv), read failures and avoid common mistakes.</description>
<content:encoded><![CDATA[<p>To validate an API response against a JSON Schema in a test, define the schema as a JavaScript object in the post-response script and assert <code>swc.response.to.have.jsonSchema(schema)</code> inside <code>swc.test</code>. Swastik Codes validates with Ajv 6, which implements JSON Schema draft-07, and the failing test lists every mismatch, such as <code>data.items[0].price should be number</code>. The same line works as <code>pm.response.to.have.jsonSchema(schema)</code> in scripts written for Postman.</p>
<p>This guide explains what JSON Schema is, which draft to write, how to build a schema for a real response, and the mistakes that make schema tests pass when they should fail.</p>
<h2 id="what-json-schema-is">What JSON Schema is</h2>
<p>JSON Schema is a vocabulary for describing the shape of JSON data: which type each value has, which properties are required, which values are allowed. A schema is itself a JSON document:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{
  &quot;type&quot;: &quot;object&quot;,
  &quot;required&quot;: [&quot;id&quot;, &quot;email&quot;],
  &quot;properties&quot;: {
    &quot;id&quot;: { &quot;type&quot;: &quot;integer&quot;, &quot;minimum&quot;: 1 },
    &quot;email&quot;: { &quot;type&quot;: &quot;string&quot;, &quot;format&quot;: &quot;email&quot; }
  }
}</code></pre></div>
<p>In API testing a schema replaces dozens of field-by-field checks. Instead of asserting that <code>id</code> is a number, <code>email</code> is a string and <code>address</code> is present, you describe the response once and validate the whole body in one test. It catches the problems individual assertions miss: a field that turned into a string, a property that disappeared, a <code>null</code> where the client expects an object.</p>
<h2 id="which-draft-to-use">Which draft to use</h2>
<p>JSON Schema has gone through several drafts. The main ones in use:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Draft</th><th>Notable changes</th><th>In the script sandbox</th></tr></thead><tbody><tr><td>draft-04 (2013)</td><td><code>id</code>, boolean <code>exclusiveMinimum</code></td><td>via the bundled <code>tv4</code> library</td></tr><tr><td>draft-06 (2017)</td><td><code>const</code>, <code>contains</code>, numeric <code>exclusiveMinimum</code></td><td>keywords work under draft-07</td></tr><tr><td>draft-07 (2018)</td><td><code>if</code>/<code>then</code>/<code>else</code>, <code>readOnly</code>, <code>writeOnly</code></td><td><strong>supported by <code>jsonSchema</code> (Ajv 6)</strong></td></tr><tr><td>2019-09</td><td><code>unevaluatedProperties</code>, <code>$defs</code>, <code>dependentRequired</code></td><td>not supported</td></tr><tr><td>2020-12</td><td><code>prefixItems</code>, <code>$dynamicRef</code></td><td>not supported</td></tr></tbody></table></div>
<p>Write draft-07 schemas and leave out <code>$schema</code>, or set it to <code>http://json-schema.org/draft-07/schema#</code>. A <code>$schema</code> pointing at draft-04, draft-06, 2019-09 or 2020-12 makes the test fail with <code>no schema with key or ref</code>, because only the draft-07 meta-schema is loaded.</p>
<p>OpenAPI uses JSON Schema too, with differences. OpenAPI 3.0 schemas are a modified subset of an older draft and add <code>nullable</code>; OpenAPI 3.1 aligns with 2020-12. Copying a schema from a spec usually works, with the caveats in the mistakes section below. If the API has an OpenAPI file, you can also have the tests generated; see <a href="https://swastik.codes/blog/test-openapi-spec/">importing an OpenAPI spec and generating tests</a>.</p>
<h2 id="write-a-schema-for-a-real-response">Write a schema for a real response</h2>
<p>Take an order endpoint that returns:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{
  &quot;id&quot;: &quot;ord_8f2k1&quot;,
  &quot;status&quot;: &quot;paid&quot;,
  &quot;createdAt&quot;: &quot;2026-10-23T09:12:44Z&quot;,
  &quot;customer&quot;: { &quot;id&quot;: 42, &quot;email&quot;: &quot;ada@example.com&quot; },
  &quot;items&quot;: [
    { &quot;sku&quot;: &quot;BK-101&quot;, &quot;quantity&quot;: 2, &quot;price&quot;: 12.5 }
  ],
  &quot;total&quot;: 25.0,
  &quot;couponCode&quot;: null
}</code></pre></div>
<p>Write the schema from the contract you expect, not just from one sample. Mark what is always present as <code>required</code>, constrain enums and patterns, and say explicitly which fields may be <code>null</code>:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const orderSchema = {
  type: 'object',
  required: ['id', 'status', 'createdAt', 'customer', 'items', 'total'],
  additionalProperties: false,
  properties: {
    id: { type: 'string', pattern: '^ord_[a-z0-9]+$' },
    status: { type: 'string', enum: ['pending', 'paid', 'shipped', 'cancelled'] },
    createdAt: { type: 'string', format: 'date-time' },
    customer: {
      type: 'object',
      required: ['id', 'email'],
      properties: {
        id: { type: 'integer', minimum: 1 },
        email: { type: 'string', format: 'email' }
      }
    },
    items: {
      type: 'array',
      minItems: 1,
      items: { $ref: '#/definitions/lineItem' }
    },
    total: { type: 'number', minimum: 0 },
    couponCode: { type: ['string', 'null'] }
  },
  definitions: {
    lineItem: {
      type: 'object',
      required: ['sku', 'quantity', 'price'],
      properties: {
        sku: { type: 'string' },
        quantity: { type: 'integer', minimum: 1 },
        price: { type: 'number', minimum: 0 }
      }
    }
  }
};</code></pre></div>
<p><code>definitions</code> with a local <code>$ref</code> lets you describe a repeated shape once. (2019-09 renamed this to <code>$defs</code>; under draft-07, <code>definitions</code> is the conventional name.)</p>
<h2 id="validate-it-in-a-test">Validate it in a test</h2>
<p>Paste the schema into the request's post-response script, in the <strong>Scripts</strong> tab, and add the test:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('status is 200', function () {
  swc.response.to.have.status(200);
});

swc.test('order matches the schema', function () {
  swc.response.to.have.jsonSchema(orderSchema);
});</code></pre></div>
<p>The editor also offers a <em>Validate JSON schema</em> snippet as a starting point. Results appear in the <strong>Test Results</strong> tab. When the body does not match, the message lists all errors, because Ajv runs with <code>allErrors</code>:</p>
<div class="code"><div class="code-head"><span>Text</span></div><pre tabindex="0"><code class="language-text">expected data to satisfy schema but found following errors:
data.status should be equal to one of the allowed values, data.items[0].quantity should be integer</code></pre></div>
<p>The paths tell you exactly where to look. If the body is not JSON at all, it is validated as <code>undefined</code> and the test fails, so keep a separate status test: the first failure then explains the second.</p>
<p>To validate part of a response, or any value, use <code>swc.expect</code>:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const body = swc.response.json();

swc.test('first item matches the line item schema', function () {
  swc.expect(body.items[0]).to.have.jsonSchema(orderSchema.definitions.lineItem);
});</code></pre></div>
<p><code>jsonSchema</code> takes Ajv options as a second argument, which is useful for the format and <code>nullable</code> cases below:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.response.to.have.jsonSchema(schema, { unknownFormats: 'ignore', nullable: true });</code></pre></div>
<p>If you prefer to call a validator directly, <code>require('ajv')</code> and <code>require('tv4')</code> are both bundled. <code>tv4</code> targets draft-04 and is mostly found in older collections:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('valid with tv4', function () {
  swc.expect(tv4.validate(swc.response.json(), schema), tv4.error &amp;&amp; tv4.error.message).to.be.true;
});</code></pre></div>
<h2 id="common-mistakes">Common mistakes</h2>
<p>Schema tests fail quietly in one direction: a loose schema passes almost anything. These are the problems that come up most.</p>
<h3 id="forgetting-required">Forgetting <code>required</code></h3>
<p><code>properties</code> only describes a property <em>if it is present</em>. Without <code>required</code>, an empty object <code>{}</code> passes the schema above. List every field the client depends on in <code>required</code>, at every level of nesting.</p>
<h3 id="leaving-additionalproperties-open-or-closing-it-in-the-wrong-place">Leaving <code>additionalProperties</code> open, or closing it in the wrong place</h3>
<p>By default extra properties are allowed, so a misspelled field (<code>totl</code> instead of <code>total</code>) passes. <code>additionalProperties: false</code> rejects unknown fields. Decide deliberately: closed schemas catch typos and accidental data leaks, open ones tolerate an API adding fields. A common compromise is closed at the top level and open in nested objects you do not own.</p>
<p><code>additionalProperties: false</code> only knows the <code>properties</code> declared next to it. If you compose a schema with <code>allOf</code>, properties defined inside the <code>allOf</code> branches count as additional and fail. Draft 2019-09 added <code>unevaluatedProperties</code> for this, but it is not available in draft-07, so repeat the property names at the level that sets <code>additionalProperties: false</code>.</p>
<h3 id="expecting-nullable-to-work">Expecting <code>nullable</code> to work</h3>
<p><code>nullable: true</code> is an OpenAPI 3.0 keyword, not JSON Schema. Ajv ignores unknown keywords by default, so <code>{ &quot;type&quot;: &quot;string&quot;, &quot;nullable&quot;: true }</code> still rejects <code>null</code>. Use a type array, <code>{ &quot;type&quot;: [&quot;string&quot;, &quot;null&quot;] }</code>, or pass <code>{ nullable: true }</code> as the Ajv option shown above when you paste schemas from an OpenAPI 3.0 file.</p>
<h3 id="formats-that-are-too-strict-or-unknown">Formats that are too strict, or unknown</h3>
<p>Ajv checks common formats such as <code>date-time</code>, <code>date</code>, <code>email</code>, <code>uri</code>, <code>uuid</code>, <code>ipv4</code> and <code>ipv6</code>. <code>date-time</code> follows RFC 3339 and requires a time zone, so <code>2026-10-23T09:12:44</code> fails while <code>2026-10-23T09:12:44Z</code> passes. That is usually a real finding about the API, not a schema bug.</p>
<p>Formats Ajv does not know, such as OpenAPI's <code>int32</code>, <code>int64</code>, <code>float</code> or <code>binary</code>, make validation throw <code>unknown format &quot;int64&quot; is used in schema</code>. Remove them, or pass <code>{ unknownFormats: 'ignore' }</code>.</p>
<h3 id="integer-versus-number"><code>integer</code> versus <code>number</code></h3>
<p><code>integer</code> rejects <code>12.5</code>, and <code>number</code> accepts both. Money is often serialized as a decimal, so check what the API really sends. Also note that <code>&quot;1&quot;</code> is a string: a schema with <code>type: 'integer'</code> correctly fails an API that quotes its numbers.</p>
<h3 id="typos-in-keywords">Typos in keywords</h3>
<p>A misspelled keyword such as <code>require</code> or <code>maxlength</code> is ignored without an error, so the constraint silently does nothing. An invalid value for a known keyword, such as <code>type: 'strin'</code>, makes the schema invalid and the test fails with <code>schema is invalid</code>. When a schema test passes suspiciously easily, send a response you know is wrong and confirm it fails.</p>
<h3 id="validating-the-wrong-root">Validating the wrong root</h3>
<p>If the endpoint returns an array, the root schema must be <code>type: 'array'</code> with <code>items</code>. A list response wrapped in an envelope (<code>{ &quot;data&quot;: [...], &quot;next&quot;: &quot;...&quot; }</code>) needs an object schema with <code>data</code> inside it.</p>
<h2 id="apply-schemas-across-a-collection">Apply schemas across a collection</h2>
<p>Copying schemas into every request is hard to maintain. Two approaches scale better.</p>
<p><strong>One collection-level script.</strong> Post-response scripts on a collection run after every request in it. Keep the schemas there, keyed by request name, and validate whichever one applies:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// Collection &gt; Scripts &gt; Post-response; define orderSchema above this line
const schemas = {
  'Get order': orderSchema,
  'List orders': { type: 'array', items: orderSchema }
};

const schema = schemas[swc.info.requestName];
if (schema) {
  swc.test('response matches the schema for ' + swc.info.requestName, function () {
    swc.response.to.have.jsonSchema(schema);
  });
}</code></pre></div>
<p><strong>Generated from a spec.</strong> If the API is described in OpenAPI, the spec view in <strong>APIs</strong> generates contract tests that validate each response against its documented schema, converted to draft-07 (<code>nullable</code> becomes a <code>null</code> type, formats are dropped). <a href="https://swastik.codes/blog/test-openapi-spec/">Importing an OpenAPI spec and generating tests</a> walks through it.</p>
<p>Then run the collection in the <a href="https://swastik.codes/features/collection-runner/">collection runner</a>. Every request's schema test shows up in the run summary, data files can drive the same requests with different inputs (<a href="https://swastik.codes/blog/data-driven-api-testing/">data-driven API testing</a>), and the results export as JUnit XML for CI. A <a href="https://swastik.codes/features/api-monitoring/">monitor</a> can run the same collection on a schedule and alert you when a response stops matching its schema.</p>
<p>Requests from the web app go through the swastik.codes server, which needs a free account and does not reach private or loopback addresses; for an API on your own machine, use the <a href="https://swastik.codes/download/">desktop app</a> for Windows or Linux.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="which-json-schema-validator-do-the-test-scripts-use">Which JSON Schema validator do the test scripts use?</h3>
<p><code>jsonSchema</code> assertions use Ajv 6 with <code>allErrors</code> enabled, which implements draft-07. <code>tv4</code> (draft-04) is also bundled for older scripts, and <code>require('ajv')</code> gives you Ajv directly.</p>
<h3 id="does-it-support-json-schema-2020-12">Does it support JSON Schema 2020-12?</h3>
<p>No. Schemas that declare <code>$schema</code> as 2019-09 or 2020-12 fail, and keywords introduced in those drafts, such as <code>unevaluatedProperties</code> and <code>prefixItems</code>, are not applied. Most response schemas can be written in draft-07 without loss.</p>
<h3 id="how-do-i-allow-null-for-a-field">How do I allow null for a field?</h3>
<p>Use a type array: <code>{ &quot;type&quot;: [&quot;string&quot;, &quot;null&quot;] }</code>. The OpenAPI <code>nullable</code> keyword only works if you pass <code>{ nullable: true }</code> as the second argument to <code>jsonSchema</code>.</p>
<h3 id="can-i-validate-a-nested-object-or-an-array-item-only">Can I validate a nested object or an array item only?</h3>
<p>Yes. Use <code>swc.expect(value).to.have.jsonSchema(schema)</code> with any value from <code>swc.response.json()</code>.</p>
<h3 id="is-schema-validation-enough-on-its-own">Is schema validation enough on its own?</h3>
<p>No. A schema checks shape and types, not meaning. Combine it with value checks: the right id came back, totals add up, the status code is the documented one. <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">Writing API tests with swc.* scripts</a> covers those assertions.</p>]]></content:encoded>
</item>
<item>
<title>REST vs GraphQL vs gRPC: differences and when to use each</title>
<link>https://swastik.codes/blog/rest-vs-graphql-vs-grpc/</link>
<guid isPermaLink="true">https://swastik.codes/blog/rest-vs-graphql-vs-grpc/</guid>
<pubDate>Fri, 23 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>REST</category><category>GraphQL</category><category>gRPC</category><category>API design</category>
<description>REST, GraphQL and gRPC compared on transport, payload, schema, streaming, caching, browser support and tooling, with a table and when to choose each.</description>
<content:encoded><![CDATA[<p>REST, GraphQL and gRPC are three ways to design an API. REST exposes resources at URLs and uses HTTP methods and status codes, usually with JSON; GraphQL exposes one endpoint where the client sends a query describing exactly which fields it wants from a typed schema; gRPC is a remote procedure call framework that calls typed methods over HTTP/2 with compact Protocol Buffers messages and built-in streaming. In short: REST is the default for public and web APIs, GraphQL suits clients with varied data needs, and gRPC suits fast, strongly typed service-to-service calls.</p>
<p>None of them is better in general. They make different trade-offs, and many systems use more than one: gRPC between internal services, REST or GraphQL at the edge. This guide compares them point by point, sums it up in a table and shows how to test each one.</p>
<h2 id="rest-in-brief">REST in brief</h2>
<p>REST (Representational State Transfer) is an architectural style described by Roy Fielding in 2000, not a protocol or a specification. In practice, a &quot;REST API&quot; means:</p>
<ul><li><strong>Resources at URLs</strong>: <code>/orders</code>, <code>/orders/42</code>, <code>/orders/42/items</code>.</li><li><strong>HTTP methods for intent</strong>: <code>GET</code> reads, <code>POST</code> creates, <code>PUT</code> replaces, <code>PATCH</code> updates, <code>DELETE</code> removes. <code>GET</code>, <code>PUT</code> and <code>DELETE</code> are idempotent by definition.</li><li><strong>HTTP status codes for outcomes</strong>: <code>200</code>, <code>201</code>, <code>404</code>, <code>409</code>, <code>422</code> and so on (see the <a href="https://swastik.codes/blog/http-status-codes/">HTTP status codes guide</a>).</li><li><strong>A representation format</strong>, almost always JSON today, negotiated with <code>Content-Type</code> and <code>Accept</code>.</li></ul>
<div class="code"><div class="code-head"><span>HTTP</span></div><pre tabindex="0"><code class="language-http">GET /v1/orders/42 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGciOi...</code></pre></div>
<p>REST has no built-in schema. Teams usually describe the contract with OpenAPI, which also drives documentation, client generation and contract tests (see <a href="https://swastik.codes/blog/test-openapi-spec/">testing an API against its OpenAPI spec</a>).</p>
<p><strong>Strengths</strong>: works everywhere HTTP works, including browsers, <code>curl</code> and every language; HTTP caching, CDNs and proxies understand it; easy to debug by reading requests. <strong>Weaknesses</strong>: fixed response shapes cause over-fetching (fields you do not need) and under-fetching (several round trips to assemble one screen); versioning and consistency depend on discipline.</p>
<h2 id="graphql-in-brief">GraphQL in brief</h2>
<p>GraphQL is a query language and runtime, published by Facebook in 2015 and now maintained under the GraphQL Foundation. The server publishes a strongly typed <strong>schema</strong>; the client sends an <strong>operation</strong> (query, mutation or subscription) that selects exactly the fields it needs.</p>
<div class="code"><div class="code-head"><span>GraphQL</span></div><pre tabindex="0"><code class="language-graphql">query Order($id: ID!) {
  order(id: $id) {
    id
    status
    customer { name email }
    items { sku quantity }
  }
}</code></pre></div>
<p>Over HTTP this is typically a <code>POST</code> to a single endpoint such as <code>/graphql</code> with a JSON body containing <code>query</code> and <code>variables</code>. Queries can also be sent with <code>GET</code>. The response is JSON with a <code>data</code> key and, when something failed, an <code>errors</code> array. Many servers return HTTP <code>200</code> even when <code>errors</code> is present, so a client or test must check the body, not only the status. The GraphQL over HTTP specification, being developed at the GraphQL Foundation, defines more precise status code behaviour for the <code>application/graphql-response+json</code> media type.</p>
<p><strong>Subscriptions</strong> (server-pushed events) usually run over WebSocket using the <code>graphql-transport-ws</code> protocol (the <code>graphql-ws</code> library) or the older <code>subscriptions-transport-ws</code> protocol; some servers use Server-Sent Events instead.</p>
<p><strong>Strengths</strong>: one round trip for nested data, no over-fetching, a self-describing schema with introspection, strong tooling for exploring and autocompleting queries, and an easy way to evolve the API by adding fields and deprecating old ones. <strong>Weaknesses</strong>: HTTP caching is harder because most traffic is <code>POST</code> to one URL; the server must defend against expensive queries (depth and complexity limits, persisted queries); naive resolvers cause N+1 database queries unless batched (for example with DataLoader); and error handling is less uniform than HTTP status codes.</p>
<h2 id="grpc-in-brief">gRPC in brief</h2>
<p>gRPC is an open-source RPC framework, released by Google in 2015 and now a Cloud Native Computing Foundation project. You define services and messages in a <code>.proto</code> file, and code generators produce typed client stubs and server interfaces in many languages.</p>
<div class="code"><div class="code-head"><span>Protobuf</span></div><pre tabindex="0"><code class="language-proto">syntax = &quot;proto3&quot;;

service OrderService {
  rpc GetOrder (GetOrderRequest) returns (Order);
  rpc WatchOrders (WatchRequest) returns (stream OrderEvent);
}

message GetOrderRequest { string id = 1; }
message Order { string id = 1; string status = 2; repeated Item items = 3; }</code></pre></div>
<p>gRPC runs over <strong>HTTP/2</strong> and encodes messages with <strong>Protocol Buffers</strong>, a compact binary format, by default. Every call is a <code>POST</code> to <code>/package.Service/Method</code>; the result is reported in a gRPC status code (<code>0 OK</code>, <code>5 NOT_FOUND</code>, <code>14 UNAVAILABLE</code>, <code>16 UNAUTHENTICATED</code> and so on) sent in HTTP/2 trailers, alongside key-value <strong>metadata</strong>. There are four call types: <strong>unary</strong> (one request, one response), <strong>server streaming</strong>, <strong>client streaming</strong> and <strong>bidirectional streaming</strong>. Clients set <strong>deadlines</strong>, and cancellation propagates to the server.</p>
<p><strong>Strengths</strong>: small payloads and efficient parsing, strict contracts with generated code, first-class streaming in both directions, and deadlines, cancellation and status codes built into the protocol. <strong>Weaknesses</strong>: browsers cannot speak native gRPC; binary payloads are not human-readable without tooling; HTTP caching and CDNs do not apply; and long-lived HTTP/2 connections need load balancing that understands HTTP/2 (layer 7) to spread calls across servers.</p>
<h3 id="grpc-in-the-browser-grpc-web">gRPC in the browser: gRPC-Web</h3>
<p>Browser JavaScript has no API to control HTTP/2 framing or read trailers, which native gRPC requires. <strong>gRPC-Web</strong> is a variant protocol that browsers can use; it normally needs a translating proxy (Envoy is the common choice) or server-side support in the framework. gRPC-Web supports unary and server-streaming calls, but not client-streaming or bidirectional streaming. Alternatives such as the Connect protocol take a similar approach. If a browser frontend is the main client, this extra layer is a real cost to weigh.</p>
<h2 id="comparison-table">Comparison table</h2>
<div class="table-wrap" tabindex="0"><table><thead><tr><th></th><th>REST</th><th>GraphQL</th><th>gRPC</th></tr></thead><tbody><tr><td>Model</td><td>Resources and HTTP methods</td><td>Typed schema, client-selected fields</td><td>Typed service methods (RPC)</td></tr><tr><td>Transport</td><td>HTTP/1.1, HTTP/2 or HTTP/3</td><td>HTTP (usually POST); WebSocket for subscriptions</td><td>HTTP/2</td></tr><tr><td>Payload</td><td>Usually JSON; any media type</td><td>JSON</td><td>Protocol Buffers (binary) by default</td></tr><tr><td>Contract</td><td>Optional (OpenAPI)</td><td>Required schema (SDL), introspectable</td><td>Required <code>.proto</code> files</td></tr><tr><td>Code generation</td><td>Optional, from OpenAPI</td><td>Optional, from the schema</td><td>Standard workflow</td></tr><tr><td>Endpoints</td><td>Many URLs</td><td>One URL</td><td>One path per method</td></tr><tr><td>Errors</td><td>HTTP status codes</td><td><code>errors</code> array in the body, often with HTTP 200</td><td>gRPC status codes in trailers</td></tr><tr><td>Streaming</td><td>Not built in; use SSE, WebSocket or chunked responses</td><td>Subscriptions (usually over WebSocket)</td><td>Server, client and bidirectional streams</td></tr><tr><td>HTTP caching</td><td>Native (<code>Cache-Control</code>, <code>ETag</code>, CDNs)</td><td>Limited; persisted queries over GET help; client caches common</td><td>Not applicable</td></tr><tr><td>Browser support</td><td>Native</td><td>Native</td><td>Needs gRPC-Web and usually a proxy</td></tr><tr><td>Human-readable on the wire</td><td>Yes</td><td>Yes</td><td>No (binary)</td></tr><tr><td>Typical use</td><td>Public APIs, web and mobile backends</td><td>Frontends with varied data needs, aggregating many services</td><td>Internal microservices, low-latency and streaming systems</td></tr></tbody></table></div>
<h2 id="when-to-choose-which">When to choose which</h2>
<p><strong>Choose REST when</strong> the API is public or consumed by many unknown clients, you want HTTP caching and CDNs to work for you, the data maps naturally onto resources, or you want the lowest barrier to entry for consumers. It remains the safest default.</p>
<p><strong>Choose GraphQL when</strong> you have several clients (web, iOS, Android, partners) that need different shapes of the same data, screens assemble data from many entities or backend services, and your team is ready to invest in query cost limits, batching and schema governance. A GraphQL layer in front of existing REST or gRPC services is a common pattern.</p>
<p><strong>Choose gRPC when</strong> both ends are services you control, latency and payload size matter, you want strict contracts with generated clients in several languages, or you need streaming in both directions. It is a frequent choice for internal microservice traffic, often behind a REST or GraphQL gateway for external clients.</p>
<p><strong>Mixing is normal.</strong> A typical layout is gRPC between internal services, a GraphQL or REST API for the frontend, and REST webhooks for partners. Choose per boundary, not per company.</p>
<h2 id="how-to-test-each-style-in-swastik-codes">How to test each style in Swastik Codes</h2>
<p>Swastik Codes has a request type for each style, and the three share environments, variables, auth helpers and <code>swc.*</code> test scripts (Postman's <code>pm.*</code> syntax also runs unchanged). Saved requests of all three types can be run in the <a href="https://swastik.codes/features/collection-runner/">collection runner</a> and on <a href="https://swastik.codes/features/api-monitoring/">monitors</a>.</p>
<h3 id="rest">REST</h3>
<p>Use the <a href="https://swastik.codes/features/rest-api-client/">REST API client</a>: pick a method, enter the URL, add query parameters, headers, a body (JSON, form-data, URL-encoded, binary) and an auth type, and send. Then assert on status, headers and the body:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('order is returned', function () {
  swc.response.to.have.status(200);
  const order = swc.response.json();
  swc.expect(order.id).to.eql('42');
});</code></pre></div>
<p>If you have an OpenAPI document, import it to get one request per operation. For step-by-step guidance, see <a href="https://swastik.codes/blog/how-to-test-rest-api-online/">how to test a REST API online</a> and <a href="https://swastik.codes/blog/json-schema-validation/">validating responses with JSON Schema</a>.</p>
<h3 id="graphql">GraphQL</h3>
<p>Create a request and switch its type to GraphQL with the <a href="https://swastik.codes/features/graphql-client/">GraphQL client</a>. Fetch the schema by introspection (or load SDL or an introspection JSON if introspection is disabled), browse types in the schema explorer, and write the query with autocomplete and a separate variables editor. Because GraphQL often returns <code>200</code> with errors, test the body:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('no GraphQL errors', function () {
  const body = swc.response.json();
  swc.expect(body.errors, JSON.stringify(body.errors)).to.be.undefined;
  swc.expect(body.data.order.status).to.be.a('string');
});</code></pre></div>
<p>Subscriptions connect over WebSocket with <code>graphql-transport-ws</code> or the legacy <code>graphql-ws</code> protocol. The <a href="https://swastik.codes/blog/graphql-client-online/">GraphQL client guide</a> walks through each step.</p>
<h3 id="grpc">gRPC</h3>
<p>Create a gRPC request with the <a href="https://swastik.codes/features/grpc-client/">gRPC client</a>. Load the service through server reflection, <code>.proto</code> files (single files or a folder, with import paths) or a <code>.proto</code> URL. Pick a method, edit the request message as JSON (an example message is generated from the schema), add metadata and a deadline, choose TLS, and invoke. Unary, server-streaming, client-streaming and bidirectional calls are all supported, and you can paste a <code>grpcurl</code> command or copy a request as one.</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('call succeeded', function () {
  swc.response.to.have.statusCode(0); // gRPC OK
  swc.expect(swc.response.messages.count()).to.equal(1);
});</code></pre></div>
<p>The web app does not need a gRPC-Web proxy, because the swastik.codes server makes native gRPC calls over HTTP/2 on your behalf. That server only reaches public hosts; for <code>localhost:50051</code> use the <a href="https://swastik.codes/download/">desktop app</a> for Windows and Linux. See the <a href="https://swastik.codes/blog/grpc-client-in-the-browser/">gRPC client guide</a> for streaming and scripts.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="is-graphql-faster-than-rest">Is GraphQL faster than REST?</h3>
<p>Not inherently. GraphQL can reduce the number of round trips and the amount of data sent, which helps on slow mobile networks, but each query may do more work on the server, and HTTP caching is harder. A well-designed REST endpoint with caching can be faster for the same screen.</p>
<h3 id="is-grpc-faster-than-rest">Is gRPC faster than REST?</h3>
<p>Usually, for service-to-service calls: Protocol Buffers are smaller and quicker to parse than JSON, and HTTP/2 multiplexes calls over one connection. The difference matters most at high request rates or with large messages. For a few calls from a browser, network latency dominates and the gain is small.</p>
<h3 id="can-a-browser-call-a-grpc-api-directly">Can a browser call a gRPC API directly?</h3>
<p>Not with native gRPC. Browsers need gRPC-Web (or a similar protocol such as Connect), which usually requires a proxy or server support, and it does not support client or bidirectional streaming. Testing tools that relay calls through a server, like the Swastik Codes web app, avoid this limitation for testing purposes.</p>
<h3 id="does-graphql-replace-rest">Does GraphQL replace REST?</h3>
<p>No. GraphQL solves specific problems, mainly varied client data needs and aggregation across services. Many organisations run both, and GraphQL servers often call REST or gRPC services behind the scenes.</p>
<h3 id="can-i-use-rest-graphql-and-grpc-in-the-same-project">Can I use REST, GraphQL and gRPC in the same project?</h3>
<p>Yes, and it is common: gRPC internally, GraphQL or REST for frontends, REST for public and partner APIs. Keep each contract in its own schema format (OpenAPI, SDL, <code>.proto</code>) and test every layer.</p>
<h3 id="which-is-best-for-real-time-data">Which is best for real-time data?</h3>
<p>gRPC streaming for service-to-service streams, GraphQL subscriptions when your API is already GraphQL, and for plain HTTP APIs, Server-Sent Events or WebSocket alongside REST. See <a href="https://swastik.codes/blog/test-server-sent-events/">testing Server-Sent Events</a> and <a href="https://swastik.codes/blog/test-websocket-api-online/">testing a WebSocket API</a>.</p>]]></content:encoded>
</item>
<item>
<title>A gRPC client in the browser: test gRPC APIs online</title>
<link>https://swastik.codes/blog/grpc-client-in-the-browser/</link>
<guid isPermaLink="true">https://swastik.codes/blog/grpc-client-in-the-browser/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>gRPC</category><category>Testing</category><category>Protobuf</category>
<description>Call gRPC services from your browser with server reflection or .proto files: unary and streaming calls, metadata, deadlines, TLS and test scripts.</description>
<content:encoded><![CDATA[<p>Browsers cannot speak native gRPC: the protocol needs full control over HTTP/2 frames and trailers, which the fetch API does not give a web page. That is why &quot;test gRPC online&quot; usually means installing a desktop tool or running <code>grpcurl</code> in a terminal. Swastik Codes takes another route. Its web app hands the call to the swastik.codes server, which makes a real gRPC call over HTTP/2 and streams the results back to your tab. You get a full gRPC client in the browser: load services by server reflection or from <code>.proto</code> files, make unary, server-streaming, client-streaming and bidirectional calls, set metadata and deadlines, and write test scripts.</p>
<p>This guide uses the public test server at <code>grpcb.in</code>, which supports server reflection on port 9000 (plaintext) and 9001 (TLS), so you can follow along before pointing the client at your own service.</p>
<h2 id="what-you-need">What you need</h2>
<ul><li>A gRPC server reachable from the public internet. The relay refuses private, loopback and other reserved addresses, so <code>localhost:50051</code> or an internal IP will not work from the web app. Use a staging host or a tunnel with a public hostname. The <a href="https://swastik.codes/download/">desktop app</a> for Windows and Linux calls services from your own machine.</li><li>Either server reflection enabled on the server, or the <code>.proto</code> files that describe it.</li><li>A free Swastik Codes account. The web app needs one, because calls run through the server.</li></ul>
<h2 id="step-1-create-a-grpc-request">Step 1: create a gRPC request</h2>
<ol><li>Open <a href="https://swastik.codes/app">the app</a> and sign in.</li><li>Click <strong>+</strong> in the tab bar and choose <strong>gRPC</strong> from the <strong>New request</strong> menu.</li><li>Enter the server address. Any of these forms work: <code>host:port</code>, <code>grpc://host:port</code> (plaintext), <code>grpcs://host:port</code> (TLS), or <code>http://</code> and <code>https://</code>. For the demo, enter <code>grpcb.in:9001</code> and turn on <strong>Use TLS</strong>, or simply enter <code>grpcs://grpcb.in:9001</code>.</li></ol>
<p>The address field also accepts a pasted <code>grpcurl</code> command, and the code button next to it copies the current call as a <code>grpcurl</code> command. That makes it easy to move between a teammate's terminal and the app.</p>
<h2 id="step-2-load-the-service-definition">Step 2: load the service definition</h2>
<p>gRPC messages are Protobuf, so the client needs the schema before it can turn your JSON into bytes. Open the <strong>Service definition</strong> tab and pick a <strong>Schema source</strong>.</p>
<h3 id="server-reflection">Server reflection</h3>
<p>Choose <strong>Server reflection</strong> and click <strong>Reflect</strong> (or <strong>Load services</strong>). Swastik Codes asks the server for its services through the reflection API (<code>grpc.reflection.v1</code>, falling back to <code>v1alpha</code> for servers that only implement the older version). With <code>grpcb.in</code> you will see services such as <code>hello.HelloService</code>, <code>grpcbin.GRPCBin</code> and <code>addsvc.Add</code>. Reflection services themselves are hidden from the list.</p>
<p>If your server requires authentication even for reflection, add the metadata first (see Step 4); it is sent with the reflection request too.</p>
<h3 id="proto-files">.proto files</h3>
<p>Many production servers disable reflection. In that case choose <strong>.proto files</strong> and:</p>
<ul><li>click <strong>Add .proto files</strong> to upload one or more files, or add a whole folder so relative import paths are kept, or</li><li>paste a URL to a raw <code>.proto</code> file and click <strong>Fetch</strong>.</li></ul>
<p>If your files import each other from different roots, list the directories under <strong>Import paths</strong>, one per line, exactly like <code>protoc -I</code>. Imports are resolved against those paths first and then by path suffix or file name. The files are compiled in memory on the server, and well-known types such as <code>google/protobuf/timestamp.proto</code> are available.</p>
<p>Click <strong>Load protos</strong>. The method dropdown in the address bar now lists every service and method, grouped by service.</p>
<h2 id="step-3-make-a-unary-call">Step 3: make a unary call</h2>
<p>Select <code>hello.HelloService/SayHello</code>. A badge next to the tabs tells you the call shape: unary, server streaming, client streaming or bidirectional streaming.</p>
<p>On the <strong>Message</strong> tab, click <strong>Use example message</strong> to get a JSON skeleton generated from the request type. For <code>SayHello</code> it has one field, <code>greeting</code>. Edit it:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{ &quot;greeting&quot;: &quot;Swastik&quot; }</code></pre></div>
<p>Press <strong>Invoke</strong>. The response panel shows the received message:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{ &quot;reply&quot;: &quot;hello Swastik&quot; }</code></pre></div>
<p>along with the response metadata (headers), trailers and the final status, for example <code>0 OK</code>, plus the time to the first message and the total duration. Variables work in the message, the metadata and the address, so <code>{&quot;greeting&quot;: &quot;{{name}}&quot;}</code> is fine.</p>
<h2 id="step-4-metadata-auth-and-deadlines">Step 4: metadata, auth and deadlines</h2>
<p>gRPC metadata is the equivalent of HTTP headers. Add keys on the <strong>Metadata</strong> tab, for example <code>x-request-id</code> or <code>x-tenant</code>. On the <strong>Authorization</strong> tab you can choose Bearer, Basic, API key or OAuth 2.0, which become metadata entries such as <code>authorization: Bearer …</code>, or inherit auth from the collection.</p>
<p>Under <strong>Settings</strong> you will find:</p>
<ul><li><strong>Deadline (ms)</strong>: the call is cancelled with <code>DEADLINE_EXCEEDED</code> if it takes longer. Set it to a small value to check how your service handles a slow downstream dependency.</li><li>TLS options: server certificate verification, a <strong>Server name override</strong> for SNI, and PEM fields for a custom CA certificate, a client certificate and a client key (for mutual TLS).</li><li><strong>Max send message size</strong> and <strong>Max receive message size</strong> in MB, a <strong>Keepalive interval</strong> for HTTP/2 pings, and whether to include fields with default values in the response. Proto3 omits default values on the wire, so turning this on shows <code>0</code>, <code>&quot;&quot;</code> and <code>false</code> fields explicitly.</li></ul>
<h2 id="step-5-streaming-calls">Step 5: streaming calls</h2>
<p>Streaming is where browser-based tools usually give up, and where the relay design pays off.</p>
<p><strong>Server streaming.</strong> Select <code>hello.HelloService/LotsOfReplies</code>, send <code>{&quot;greeting&quot;: &quot;Swastik&quot;}</code> and press <strong>Invoke</strong>. The server sends ten replies, and each one appears in the log as it arrives, followed by the final status.</p>
<p><strong>Client streaming and bidirectional streaming.</strong> Select <code>LotsOfGreetings</code> or <code>BidiHello</code>. Now <strong>Invoke</strong> opens the stream without sending anything yet. Write a message and click <strong>Send message</strong> as often as you like; each message is sent on the open stream. Click <strong>End stream</strong> to half-close your side. For a bidirectional method you see the server's replies interleaved with your messages; for client streaming the single reply arrives after you end the stream.</p>
<p>Saved messages help here. Click save in the composer to keep a message with the request, then send it again from the <strong>Saved</strong> tab in one click.</p>
<h2 id="step-6-write-grpc-tests">Step 6: write gRPC tests</h2>
<p>The <strong>Scripts</strong> tab has three hooks, each with a snippet menu:</p>
<ul><li><strong>Before invoke</strong> runs before the call. Change <code>swc.request.metadata</code> or set variables, for example to compute a signature.</li><li><strong>On message</strong> runs for every message the server sends, with the message in <code>swc.message</code>.</li><li><strong>After response</strong> runs once the call has ended, with the status, metadata, trailers and all messages in <code>swc.response</code>.</li></ul>
<p>A typical After response script for the server-streaming call above:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-js">swc.test('Status code is 0 (OK)', function () {
  swc.response.to.have.statusCode(0);
});

swc.test('Ten replies', function () {
  swc.expect(swc.response.messages.count()).to.equal(10);
});

swc.test('Every message has a reply', function () {
  swc.response.messages.to.have.property('reply');
});

swc.test('Fast enough', function () {
  swc.response.to.have.responseTime.below(2000);
});</code></pre></div>
<p>And an On message check that runs per message:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-js">swc.test('Reply mentions the greeting', function () {
  swc.expect(swc.message.data.reply).to.include('Swastik');
});</code></pre></div>
<p>Results appear in the response panel as passed and failed tests. Because gRPC requests are saved in collections like any other request, the <a href="https://swastik.codes/features/collection-runner/">collection runner</a> and <a href="https://swastik.codes/features/api-monitoring/">monitors</a> run them too, scripts included. The syntax is the same <code>pm.*</code> API used for HTTP tests; see <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">writing API tests with pm scripts</a> for the full picture.</p>
<h2 id="a-minimal-grpc-server-with-reflection">A minimal gRPC server with reflection</h2>
<p>To test your own service, enable reflection in development and staging. In Node.js with <code>@grpc/grpc-js</code>:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-js">// server.mjs  (npm install @grpc/grpc-js @grpc/proto-loader @grpc/reflection)
import * as grpc from '@grpc/grpc-js';
import * as loader from '@grpc/proto-loader';
import { ReflectionService } from '@grpc/reflection';

const def = loader.loadSync('greeter.proto', { keepCase: true });
const proto = grpc.loadPackageDefinition(def);

const server = new grpc.Server();
server.addService(proto.demo.Greeter.service, {
  sayHello(call, callback) {
    callback(null, { message: `Hello, ${call.request.name}` });
  },
});
new ReflectionService(def).addToServer(server);

server.bindAsync(`0.0.0.0:${process.env.PORT || 50051}`, grpc.ServerCredentials.createInsecure(), (err) =&gt; {
  if (err) throw err;
  console.log('gRPC server ready');
});</code></pre></div>
<p>with <code>greeter.proto</code>:</p>
<div class="code"><div class="code-head"><span>Protobuf</span></div><pre tabindex="0"><code class="language-protobuf">syntax = &quot;proto3&quot;;
package demo;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }</code></pre></div>
<p>In Go, register <code>reflection.Register(s)</code> from <code>google.golang.org/grpc/reflection</code>; in Python, use <code>grpc_reflection.v1alpha.reflection.enable_server_reflection</code>. Put the server behind a TLS-terminating proxy that supports HTTP/2 end to end, or serve TLS from the process itself.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<p><strong>&quot;Load services first&quot; stays in the method list.</strong> Reflection or proto compilation has not run yet, or it failed. Check the error under the <strong>Service definition</strong> tab.</p>
<p><strong>Reflection fails with <code>UNIMPLEMENTED</code>.</strong> The server does not have reflection enabled. Upload <code>.proto</code> files instead.</p>
<p><strong>The call fails with <code>UNAVAILABLE</code> straight away.</strong> Usually a TLS mismatch: TLS on for a plaintext port, or off for a TLS port. Many gRPC hosts behind a load balancer only accept TLS on 443.</p>
<p><strong>Import errors when loading protos.</strong> Add the directory that the <code>import</code> statements are relative to under <strong>Import paths</strong>, or add the whole folder so relative paths are kept.</p>
<p><strong><code>RESOURCE_EXHAUSTED</code> on large messages.</strong> Raise the max send or receive message size in <strong>Settings</strong>. The relay caps a single message at 16 MiB.</p>
<p><strong>Fields with zero values are missing from the response.</strong> That is proto3 behaviour. Turn on the option to include default-valued fields.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="can-a-browser-make-grpc-calls-directly">Can a browser make gRPC calls directly?</h3>
<p>Not native gRPC. Browsers do not expose the HTTP/2 trailers that gRPC relies on, which is why gRPC-Web and proxies exist. Swastik Codes runs the real gRPC call on its server and streams the results to the browser, so your service does not need gRPC-Web.</p>
<h3 id="do-i-need-server-reflection">Do I need server reflection?</h3>
<p>No. Reflection is the quickest way to load a schema, but you can always upload <code>.proto</code> files or fetch them from a URL instead.</p>
<h3 id="are-streaming-rpcs-supported">Are streaming RPCs supported?</h3>
<p>Yes: unary, server streaming, client streaming and bidirectional streaming. For streams that you send on, Invoke opens the stream, you send messages one by one, and End stream half-closes it.</p>
<h3 id="can-i-test-grpc-services-on-localhost">Can I test gRPC services on localhost?</h3>
<p>Not from the web app, because the relay refuses private and loopback addresses. Use the <a href="https://swastik.codes/download/">desktop app</a>, or expose the service on a public hostname for testing.</p>
<h3 id="can-i-run-grpc-tests-on-a-schedule">Can I run gRPC tests on a schedule?</h3>
<p>Yes. gRPC requests saved in a collection run in the collection runner and in scheduled monitors, with their scripts, so you can track a gRPC API over time and get an email or webhook alert when it fails.</p>
<h2 id="try-it">Try it</h2>
<p>Open <a href="https://swastik.codes/app">Swastik Codes</a>, create a gRPC request, enter <code>grpcs://grpcb.in:9001</code>, reflect and invoke <code>SayHello</code>. You will need a <a href="https://swastik.codes/signup">free account</a> to send calls. The <a href="https://swastik.codes/features/grpc-client/">gRPC client feature page</a> summarises every option, and if your service also exposes HTTP or GraphQL endpoints, the <a href="https://swastik.codes/features/rest-api-client/">REST API client</a> and the <a href="https://swastik.codes/blog/graphql-client-online/">GraphQL client</a> live in the same workspace.</p>]]></content:encoded>
</item>
<item>
<title>API load testing in the browser: virtual users, profiles and percentiles</title>
<link>https://swastik.codes/blog/api-load-testing-in-the-browser/</link>
<guid isPermaLink="true">https://swastik.codes/blog/api-load-testing-in-the-browser/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>Performance</category><category>Load testing</category><category>Testing</category>
<description>Load test an API online with virtual users, ramp-up, spike and peak profiles, then read throughput, p95/p99 latency and error rate correctly.</description>
<content:encoded><![CDATA[<p>You can load test an API from the browser in Swastik Codes without installing anything: pick a collection, choose how many virtual users to run and for how long, pick a load profile (fixed, ramp-up, spike or peak) and press <strong>Run</strong>. The load is generated by the swastik.codes server, and the results stream back live as charts of throughput, response-time percentiles and error rate, with a per-request breakdown at the end.</p>
<p>This guide explains how the performance runner works, walks through a first test, and spends most of its time on the part that matters: reading the numbers correctly and knowing when a browser-based load test is enough and when it is not.</p>
<aside class="callout callout-warning"><p><strong>Warning:</strong> Only load test systems you own or have written permission to test. Sending heavy traffic to someone else's API can look like a denial-of-service attack. Our <a href="https://swastik.codes/acceptable-use/">acceptable use policy</a> forbids it.</p></aside>
<h2 id="how-the-performance-runner-works">How the performance runner works</h2>
<p>The model is simple and matches what most API tools call a &quot;performance runner&quot;:</p>
<ul><li>A <strong>virtual user</strong> (VU) runs the selected requests in collection order, then starts again from the top, until the test ends or it is asked to stop.</li><li>Each VU has its own variable scope, so a token saved by a login request in one VU's loop stays available for that VU's next requests. Pre-request and test scripts run for every request, exactly as they do in a normal send.</li><li>If you attach a data file (CSV or JSON), VU number <em>n</em> uses row <em>n</em> modulo the number of rows. Ten VUs and five rows means each row is used by two VUs.</li><li>The <strong>load profile</strong> decides how many VUs are active at each moment. The controller re-checks it every 100 ms, starting new VUs or asking the newest ones to stop after their current request.</li><li>A sample counts as an <strong>error</strong> when there is no response, a transport error (DNS, TLS, timeout, connection reset), or an HTTP status of 400 or higher.</li><li>When the duration is over, requests still in flight are abandoned and not counted, so the test never overruns.</li><li>All VUs share one connection pool and one cookie jar.</li></ul>
<h3 id="the-four-load-profiles">The four load profiles</h3>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Profile</th><th>Shape</th><th>Good for</th></tr></thead><tbody><tr><td><strong>Fixed</strong></td><td>The full number of VUs for the whole duration</td><td>A steady baseline you can compare between releases</td></tr><tr><td><strong>Ramp up</strong></td><td>From <em>start users</em> up to the maximum over <em>ramp-up</em> seconds, then hold</td><td>Finding the point where latency starts to climb</td></tr><tr><td><strong>Spike</strong></td><td><em>Base users</em> all the time, the maximum for <em>spike length</em> seconds starting at <em>spike at</em></td><td>Checking how the API copes with, and recovers from, a sudden burst</td></tr><tr><td><strong>Peak</strong></td><td>Ramp from <em>base users</em> to the maximum over <em>ramp</em>, hold for <em>peak hold</em>, ramp back down</td><td>A rehearsal of a busy period, such as a launch or a sale</td></tr></tbody></table></div>
<p>The setup panel draws the profile as a small chart before you run, so you can see exactly how many VUs will be active at each second.</p>
<h3 id="limits-on-the-hosted-service">Limits on the hosted service</h3>
<p>The web app runs load tests on the swastik.codes server, so there are caps to keep the service fair for everyone. Currently a run can use up to 50 virtual users and last up to 300 seconds, each user can start up to 10 runs a minute, and one run per user can be active at a time. These limits may change. TLS certificate checks cannot be switched off for server-side load tests.</p>
<p>As with every request sent from the web app, targets must be publicly reachable: private, loopback and reserved addresses are refused. The load comes from our server's IP address, not yours, which matters if your API has an IP allowlist or a WAF that rate-limits by address.</p>
<h2 id="step-by-step-your-first-load-test">Step by step: your first load test</h2>
<h3 id="1-prepare-a-collection">1. Prepare a collection</h3>
<p><a href="https://swastik.codes/app">Open the app</a> and <a href="https://swastik.codes/signup">create a free account</a> (sending requests from the web app requires one). Put the requests you want to test in a collection, or in a folder of it. A realistic test usually has a small flow rather than a single endpoint, for example:</p>
<ol><li><code>POST {{baseUrl}}/auth/login</code>, with a test script that saves the token;</li><li><code>GET {{baseUrl}}/products?page=1</code>;</li><li><code>GET {{baseUrl}}/products/{{productId}}</code>.</li></ol>
<p>The login test script might be:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test(&quot;login succeeded&quot;, () =&gt; swc.response.to.have.status(200));
const { token } = swc.response.json();
swc.environment.set(&quot;token&quot;, token);</code></pre></div>
<p>and the other requests use <code>Bearer {{token}}</code> in their auth. Every VU works on its own copy of the variables, so each VU logs in and keeps its own token; nothing is written back to your saved environment.</p>
<p>Send each request once by hand first. A load test of a request that fails with <code>401</code> only tells you that it fails very quickly.</p>
<h3 id="2-open-a-performance-test">2. Open a performance test</h3>
<p>Click <strong>New</strong> in the top bar and choose <strong>New performance test</strong> (it is also in the command palette, Ctrl+K / Cmd+K). In the <strong>Requests</strong> section choose the collection, optionally a folder (&quot;Entire collection&quot; by default), tick the requests to include, and pick an environment.</p>
<h3 id="3-set-the-load">3. Set the load</h3>
<p>In the <strong>Load</strong> section set <strong>Virtual users</strong> and <strong>Duration</strong>, then pick a profile and fill in its fields. For a first run, try something modest:</p>
<ul><li>profile <strong>Ramp up</strong>, start users 1, ramp-up 30 s;</li><li>20 virtual users;</li><li>duration 120 s.</li></ul>
<p>Optionally attach a <strong>data file</strong> if each VU should use different input, such as different user accounts.</p>
<h3 id="4-run-and-watch">4. Run and watch</h3>
<p>Press <strong>Run</strong>. While it runs you see live totals (total requests, throughput, average response, P50, P90, P95, P99, error rate and current virtual users) and three charts: throughput with VUs, response time (average and percentiles), and error rate. When it finishes, a table lists each request with its count, requests per second, average, minimum, maximum, P50, P90, P95, P99 and errors. Distinct error messages are listed underneath.</p>
<h3 id="5-compare-and-keep-the-results">5. Compare and keep the results</h3>
<p>Finished runs are saved under <strong>Previous runs</strong> in your browser (the most recent 30 per workspace). Open an earlier run and choose it as a <strong>baseline</strong>: the charts overlay the baseline as dashed lines, and a comparison table shows whether throughput, latency and errors got better or worse. <strong>Export JSON</strong> downloads the full result if you want to keep it with a release.</p>
<h2 id="reading-the-numbers">Reading the numbers</h2>
<p>Most mistakes in load testing come from reading the results wrongly, not from running the test wrongly.</p>
<h3 id="throughput-is-an-output-not-an-input">Throughput is an output, not an input</h3>
<p>You set virtual users, not requests per second. Each VU sends its next request as soon as the previous one finishes, so throughput is roughly <code>VUs / average response time</code>. Twenty VUs against an endpoint that answers in 100 ms give at most about 200 requests per second. If the API slows to 400 ms, the same twenty VUs produce only 50 requests per second. A drop in throughput at constant VUs is therefore a sign of slower responses, not of fewer users.</p>
<h3 id="use-percentiles-not-the-average">Use percentiles, not the average</h3>
<p>The average hides the experience of the slowest users. Percentiles do not:</p>
<ul><li><strong>P50</strong> (the median): half the requests were faster than this. It describes the typical request.</li><li><strong>P95</strong>: 1 request in 20 was slower than this. This is where queueing and lock contention usually show up first.</li><li><strong>P99</strong>: 1 request in 100 was slower. If one page makes ten API calls, a large share of page views include at least one call this slow.</li></ul>
<p>A healthy result has P95 and P99 reasonably close to P50 and stable over the run. A P99 several times higher than P50, or percentiles that climb steadily while VUs are flat, points at a saturated resource: a connection pool, a database, a thread pool or a downstream service.</p>
<h3 id="watch-the-error-rate-alongside-latency">Watch the error rate alongside latency</h3>
<p>A fast run with a 30% error rate is not a fast API, it is an API that is rejecting work quickly. Check the error list: <code>429</code> means a rate limiter is doing its job, <code>502</code>/<code>503</code>/<code>504</code> usually mean an upstream or a load balancer gave up, and timeouts mean the API never answered. Remember that errors here are transport failures and HTTP status codes of 400 and above. A failing <code>swc.test</code> assertion on a <code>200</code> response is not counted as an error, so check your functional behaviour with the <a href="https://swastik.codes/features/collection-runner/">collection runner</a> as well.</p>
<h3 id="look-at-the-shape-over-time">Look at the shape over time</h3>
<p>The charts matter more than the totals. Three patterns are worth recognising:</p>
<ol><li><strong>Latency rises with VUs, then flattens while throughput stops growing.</strong> You found the capacity limit. The VU count where the curve bends is a useful number to write down.</li><li><strong>Latency spikes during a spike profile, then recovers.</strong> Good: the system absorbs bursts. If it does not recover after the spike ends, something is stuck, such as a queue that never drains.</li><li><strong>Errors start late in a fixed-load run.</strong> Something accumulates: memory, open connections, or a cache that fills up. Run longer to confirm.</li></ol>
<h3 id="be-careful-with-conclusions">Be careful with conclusions</h3>
<p>The load comes from one region and one server, over the public internet. Measured latency includes the network path between our server and your API. Compare runs with each other, from the same setup, rather than treating one number as the truth about production. And keep the test environment as close to production as you can: a staging database with a hundred rows will not behave like one with ten million.</p>
<h2 id="when-to-graduate-to-a-dedicated-load-testing-tool">When to graduate to a dedicated load-testing tool</h2>
<p>A browser-based performance runner is the right tool for quick checks: &quot;did this release make the search endpoint slower?&quot;, &quot;does the API hold up with 50 concurrent users?&quot;, &quot;what happens during a burst?&quot;. It reuses the collection, variables and scripts you already have, so there is nothing to write.</p>
<p>For larger or more formal tests, dedicated tools such as k6, Gatling, Apache JMeter and Locust are a better fit. They can generate far more load, run from several machines or regions, model arrival rates (requests per second) rather than only concurrent users, run inside CI with pass/fail thresholds, and export to monitoring systems. They take more setup, and the tests are written as code or in their own formats, but they are built for exactly that job. A common approach is to use Swastik Codes to explore and to catch regressions early, and a dedicated tool for capacity planning before a big launch.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="is-load-testing-in-swastik-codes-free">Is load testing in Swastik Codes free?</h3>
<p>Yes. Swastik Codes is free to use today. Runs are limited (currently up to 50 virtual users and 300 seconds on the hosted service) to keep it fair for everyone.</p>
<h3 id="can-i-load-test-an-api-running-on-localhost">Can I load test an API running on localhost?</h3>
<p>Not from the web app. Tests run on the swastik.codes server, which refuses private and loopback addresses. Use a publicly reachable staging URL, or run the load test in the <a href="https://swastik.codes/download/">desktop app</a>, where virtual users run on your own computer.</p>
<h3 id="do-my-test-scripts-run-during-a-load-test">Do my test scripts run during a load test?</h3>
<p>Yes. Pre-request and test scripts run for every request in every VU, so you can log in, save tokens and chain requests. Script-heavy collections add some overhead on the client side of the test.</p>
<h3 id="what-counts-as-an-error">What counts as an error?</h3>
<p>No response, a transport error, or an HTTP status of 400 or above. Failing test assertions are not counted as load-test errors.</p>
<h3 id="can-i-compare-two-runs">Can I compare two runs?</h3>
<p>Yes. Choose an earlier run as the baseline to overlay it on the charts and see the differences in a table.</p>
<h2 id="run-your-first-test">Run your first test</h2>
<p><a href="https://swastik.codes/app">Open the app</a>, <a href="https://swastik.codes/signup">create a free account</a>, and start with a small ramp-up test against an endpoint you own. The <a href="https://swastik.codes/features/load-testing/">load testing feature page</a> summarises the options, <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">Writing API tests with swc.* scripts</a> shows how to prepare tokens and assertions, and <a href="https://swastik.codes/blog/data-driven-api-testing/">data-driven API testing</a> explains data files in more detail.</p>]]></content:encoded>
</item>
<item>
<title>Data-driven API testing with CSV and JSON files</title>
<link>https://swastik.codes/blog/data-driven-api-testing/</link>
<guid isPermaLink="true">https://swastik.codes/blog/data-driven-api-testing/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>API testing</category><category>Collection runner</category><category>CSV</category><category>Tutorial</category>
<description>Run the same API requests and tests once per row of a CSV or JSON file: data variables, swc.iterationData, iterations, delays, bail options and reports.</description>
<content:encoded><![CDATA[<p>Data-driven API testing means writing a request and its tests once, then running them many times with different inputs taken from a file. Each row of a CSV or JSON file becomes one iteration. Inside the requests, <code>{{columnName}}</code> is replaced with that row's value; inside scripts, <code>swc.iterationData.get('columnName')</code> returns it. The collection runner repeats the selected requests once per row and reports every result.</p>
<p>In Swastik Codes you open a collection's menu, choose <strong>Run collection</strong>, attach the file under <strong>Data file</strong>, and press <strong>Run</strong>. This guide builds a small working example, then covers iterations, delays, failure handling, variable persistence, reports and scheduled runs.</p>
<h2 id="when-data-driven-tests-are-worth-it">When data-driven tests are worth it</h2>
<p>Data files shine when the logic is the same and only the inputs and expected outputs change:</p>
<ul><li>boundary values for a validation endpoint (empty strings, maximum lengths, invalid formats),</li><li>one test per customer tier, region, currency or feature flag,</li><li>regression checks for a list of known ids after a migration,</li><li>the same smoke test against many tenants.</li></ul>
<p>If each case needs different logic, separate requests are clearer. If only the data changes, a data file keeps the collection small and the cases easy to review.</p>
<h2 id="before-you-start">Before you start</h2>
<p>Runs from the web app are executed by the swastik.codes server: the browser streams the results as they happen. That needs a free account, and the server refuses private and loopback addresses, so the API under test must be publicly reachable. The example below uses the public <code>https://jsonplaceholder.typicode.com</code> API.</p>
<h2 id="step-1-write-the-request-with-data-variables">Step 1: write the request with data variables</h2>
<p>Create a collection called <em>Users</em> with one GET request named <em>Get user</em>:</p>
<div class="code"><div class="code-head"><span>Text</span></div><pre tabindex="0"><code class="language-text">GET https://jsonplaceholder.typicode.com/users/{{userId}}</code></pre></div>
<p><code>{{userId}}</code> is not defined in any environment; it will come from the data file. If you send the request by hand now, the variable is unresolved and the editor highlights it as missing. That is expected.</p>
<h2 id="step-2-write-tests-that-read-the-row">Step 2: write tests that read the row</h2>
<p>In the request's <strong>Scripts</strong> tab, add a Post-response script that compares the response with the expected values in the same row:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const row = {
  userId: swc.iterationData.get('userId'),
  expectedStatus: Number(swc.iterationData.get('expectedStatus')),
  expectedName: swc.iterationData.get('expectedName'),
  expectedUsername: swc.iterationData.get('expectedUsername')
};

swc.test(`user ${row.userId}: status ${row.expectedStatus}`, function () {
  swc.response.to.have.status(row.expectedStatus);
});

if (row.expectedStatus === 200) {
  const user = swc.response.json();

  swc.test(`user ${row.userId}: name is ${row.expectedName}`, function () {
    swc.expect(user.name).to.eql(row.expectedName);
  });

  swc.test(`user ${row.userId}: username is ${row.expectedUsername}`, function () {
    swc.expect(user.username).to.eql(row.expectedUsername);
  });
}</code></pre></div>
<p>Two details matter:</p>
<ul><li><strong>CSV values are strings.</strong> As in Postman, every value read from a CSV file is a string, so <code>expectedStatus</code> is converted with <code>Number()</code> before comparing with the numeric status code. JSON data files keep their types.</li><li><strong>Put the row value in the test name.</strong> The run results list tests by name, so <code>user 3: name is Clementine Bauch</code> tells you immediately which row failed.</li></ul>
<h2 id="step-3-create-the-data-file">Step 3: create the data file</h2>
<p>Save this as <code>users.csv</code>:</p>
<div class="code"><div class="code-head"><span>Text</span></div><pre tabindex="0"><code class="language-text">userId,expectedStatus,expectedName,expectedUsername
1,200,Leanne Graham,Bret
2,200,Ervin Howell,Antonette
3,200,Clementine Bauch,Samantha
11,404,,</code></pre></div>
<p>The first line is the header: each column name becomes a variable. The last row checks that a non-existent user returns 404. JSONPlaceholder has ten users, so id 11 is not found.</p>
<p>The parser handles quoted fields (<code>&quot;Smith, John&quot;</code>), escaped quotes (<code>&quot;&quot;</code>), line breaks inside quotes, Windows line endings and a byte-order mark, and detects semicolon- or tab-separated files from the header line.</p>
<p>The same data as JSON is an array of objects:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">[
  { &quot;userId&quot;: 1, &quot;expectedStatus&quot;: 200, &quot;expectedName&quot;: &quot;Leanne Graham&quot;, &quot;expectedUsername&quot;: &quot;Bret&quot; },
  { &quot;userId&quot;: 2, &quot;expectedStatus&quot;: 200, &quot;expectedName&quot;: &quot;Ervin Howell&quot;, &quot;expectedUsername&quot;: &quot;Antonette&quot; },
  { &quot;userId&quot;: 3, &quot;expectedStatus&quot;: 200, &quot;expectedName&quot;: &quot;Clementine Bauch&quot;, &quot;expectedUsername&quot;: &quot;Samantha&quot; },
  { &quot;userId&quot;: 11, &quot;expectedStatus&quot;: 404 }
]</code></pre></div>
<p>JSON is the better choice when values are numbers, booleans or nested objects you want to post as a body, for example <code>{{payload}}</code> filled from a nested object.</p>
<h2 id="step-4-run-it">Step 4: run it</h2>
<ol><li>In the sidebar, open the <em>Users</em> collection's menu and choose <strong>Run collection</strong> (or <strong>Run folder</strong> on a folder).</li><li>The runner tab shows the requests in run order. Untick any you want to leave out and use <strong>Move up</strong> / <strong>Move down</strong> to change the order.</li><li>Under <strong>Data file</strong>, choose <strong>Select file</strong> and pick <code>users.csv</code>. A preview table shows the parsed rows, and <strong>Iterations</strong> is set to the number of rows (4).</li><li>Pick an <strong>Environment</strong> if your requests use one.</li><li>Press <strong>Run</strong>.</li></ol>
<p>Results stream in as each request finishes. The summary shows requests, failed requests, tests passed and failed, the average response time and the total duration. The results table lists each iteration and request with its status, time, size and tests; filter it to failed requests, or to a single iteration, and open any row to see its tests, console output and (if saved) the response.</p>
<h2 id="iterations-delays-and-stopping-on-failure">Iterations, delays and stopping on failure</h2>
<p><strong>Iterations</strong> defaults to the number of data rows, or 1 without a file. If you set more iterations than there are rows, the last row is reused for the extra iterations, which matches Postman's runner and Newman.</p>
<p><strong>Delay (ms)</strong> waits between requests. Use it for rate-limited APIs: with a limit of 60 requests per minute, a 1000 ms delay keeps a long run under it.</p>
<p><strong>Stop the run</strong> controls failure handling:</p>
<ul><li><strong>Never (run every request)</strong> – the default; you get the complete picture.</li><li><strong>On the first failure (test or error)</strong> – stop at the first failed test or error.</li><li><strong>On the first failed test</strong> – stop on assertion failures only.</li><li><strong>On the first error (send or script)</strong> – stop when a request cannot be sent or a script throws, but keep going through failed assertions.</li></ul>
<p>The hosted service caps a run at 1,000 iterations and 10,000 data rows, and the delay at 60 seconds.</p>
<h2 id="variables-across-requests-and-iterations">Variables across requests and iterations</h2>
<p>Data-driven runs often chain requests: create something with the row's data, then read it back. The run options control how script-set variables behave:</p>
<ul><li><strong>Persist variables between requests</strong> – within one iteration, variable changes made by scripts always carry to the next request; with this on they also carry into later iterations. Turn it off if every row should start from the same values.</li><li><strong>Keep variable values after the run</strong> – write the final values back to the environment, globals and collection when the run ends. Leave it off if you do not want a test run to change your working environment.</li></ul>
<p>A chained example with two requests, <em>Create post</em> and <em>Get post</em>:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// Create post – Body (raw JSON):
// { &quot;title&quot;: &quot;{{title}}&quot;, &quot;body&quot;: &quot;{{body}}&quot;, &quot;userId&quot;: {{userId}} }

// Create post – Post-response script
swc.test(`created &quot;${swc.iterationData.get('title')}&quot;`, function () {
  swc.response.to.have.status(201);
});
swc.variables.set('createdId', swc.response.json().id);</code></pre></div>
<p><code>swc.variables.set</code> writes a local variable that lasts for the run, so the next request in the same iteration can use <code>{{createdId}}</code>.</p>
<p>Scripts can also change the flow per row. <code>swc.execution.skipRequest()</code> in a pre-request script skips a request for rows that do not apply:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// Pre-request: only rows marked as admin cases call the admin endpoint
if (swc.iterationData.get('role') !== 'admin') {
  swc.execution.skipRequest();
}</code></pre></div>
<p>Skipped requests are counted separately in the results, so they do not look like passes.</p>
<h2 id="cookies-responses-and-logs">Cookies, responses and logs</h2>
<p>Three more options are useful on long runs:</p>
<ul><li><strong>Save responses</strong> stores response headers and bodies in the results so you can inspect failures afterwards. Turn it off for very large runs.</li><li><strong>Run without stored cookies</strong> starts with an empty cookie jar; <strong>Save cookies after run</strong> writes cookies the run received back to the cookie manager. Every request in one run shares a single cookie jar.</li><li><strong>Turn off logs during run</strong> drops <code>console.log</code> output, which speeds up long runs.</li></ul>
<h2 id="reports-for-ci-and-for-people">Reports for CI and for people</h2>
<p>When the run finishes, export it as:</p>
<ul><li><strong>JSON</strong> – the full result for your own tooling,</li><li><strong>JUnit</strong> – XML that CI systems and test dashboards understand,</li><li><strong>HTML</strong> – a self-contained report to attach to a ticket or send to a colleague.</li></ul>
<p>Past runs are kept in the runner's run history on your device, so you can compare a failing run with the last good one.</p>
<h2 id="run-it-on-a-schedule">Run it on a schedule</h2>
<p>A data-driven collection makes a good monitor. Monitors run a collection on a cron schedule from the server and alert by email or a signed webhook, on failures, on every run, or when a failing monitor recovers. In a monitor's advanced settings you can attach iteration data (up to 1,000 rows), set the number of iterations, choose which requests run and in which order, and set retries and timeouts. See <a href="https://swastik.codes/features/api-monitoring/">API monitoring</a>.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<p><strong>Every iteration uses the same value.</strong> The column name in the file and the variable name in the request differ (check case and stray spaces in the header row), or an environment or collection variable with the same name is masking the data. Data values take precedence over environment, collection and global variables, but a local variable set by a script (<code>swc.variables.set</code>) takes precedence over data.</p>
<p><strong>A numeric comparison fails with matching values.</strong> CSV values are strings. Convert with <code>Number()</code> or compare with string literals, or use a JSON data file.</p>
<p><strong>The run stops early.</strong> Check <strong>Stop the run</strong>, and whether a script calls <code>swc.execution.setNextRequest(null)</code>, which ends the current iteration.</p>
<p><strong>&quot;too_many_iterations&quot; or &quot;too_much_data&quot;.</strong> The run exceeds 1,000 iterations or 10,000 rows. Split the data file.</p>
<p><strong>Requests fail to send.</strong> If the target is <code>localhost</code> or a private address, the server refuses it; use a publicly reachable environment.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="what-file-formats-can-the-collection-runner-read">What file formats can the collection runner read?</h3>
<p>CSV (comma, semicolon or tab separated, with a header row) and JSON arrays of objects.</p>
<h3 id="how-do-i-read-a-data-value-in-a-script">How do I read a data value in a script?</h3>
<p><code>swc.iterationData.get('column')</code>. <code>swc.iterationData.toObject()</code> returns the whole row. In requests, use <code>{{column}}</code>.</p>
<h3 id="is-this-compatible-with-postman-data-files">Is this compatible with Postman data files?</h3>
<p>Yes. The same CSV and JSON files work, values from CSV are strings as in Postman, and extra iterations reuse the last row as Newman does.</p>
<h3 id="can-i-use-a-data-file-for-load-tests">Can I use a data file for load tests?</h3>
<p>Yes. Performance tests can also take a data file, so each virtual user works through realistic inputs. See <a href="https://swastik.codes/blog/api-load-testing-in-the-browser/">API load testing in the browser</a>.</p>
<h2 id="try-it">Try it</h2>
<p><a href="https://swastik.codes/app">Open the app</a>, create the <em>Users</em> collection above, attach <code>users.csv</code> and run it. Runs need a <a href="https://swastik.codes/signup">free account</a>. The <a href="https://swastik.codes/features/collection-runner/">collection runner feature page</a> lists every option, <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">writing API tests with swc.* scripts</a> covers the assertions in more depth, and <a href="https://swastik.codes/features/api-monitoring/">API monitoring</a> explains how to run the same collection every few minutes.</p>]]></content:encoded>
</item>
<item>
<title>GraphQL client online: test a GraphQL API in the browser</title>
<link>https://swastik.codes/blog/graphql-client-online/</link>
<guid isPermaLink="true">https://swastik.codes/blog/graphql-client-online/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>GraphQL</category><category>API testing</category><category>Tutorial</category>
<description>Test a GraphQL API from your browser: load the schema by introspection, build queries with autocomplete, pass variables, subscribe and write tests.</description>
<content:encoded><![CDATA[<p>To test a GraphQL API online you need four things: somewhere to type the operation, the server's schema (so you get autocomplete and validation), a place for variables and headers, and a readable response. Swastik Codes gives you all four in the browser. Open <a href="https://swastik.codes/app">the app</a>, add a GraphQL request, paste the endpoint URL, and the schema is fetched by introspection. You then build the query by ticking fields in the explorer or by typing with schema-aware completion, and send it.</p>
<p>This guide walks through that flow against a public GraphQL API, then covers variables, fragments, subscriptions, authentication headers and automated tests on GraphQL responses.</p>
<h2 id="what-you-need-before-you-start">What you need before you start</h2>
<ul><li><strong>A GraphQL endpoint that is reachable from the internet.</strong> The web app sends requests through the swastik.codes server (a browser cannot call arbitrary cross-origin APIs), and that server refuses private, loopback and link-local addresses. <code>http://localhost:4000/graphql</code> will not work from the web app; a staging URL will. The <a href="https://swastik.codes/download/">desktop app</a> for Windows and Linux sends from your own machine and reaches it.</li><li><strong>A free account.</strong> The web app needs one, because requests (and schema fetches) go through the server. <a href="https://swastik.codes/signup">Create one</a> with GitHub or an email address.</li></ul>
<p>For the examples below we use the public Countries API at <code>https://countries.trevorblades.com/</code>. It allows introspection and needs no key, which makes it a good playground.</p>
<h2 id="step-1-create-a-graphql-request">Step 1: create a GraphQL request</h2>
<ol><li>Open <a href="https://swastik.codes/app">swastik.codes/app</a>.</li><li>Click the <strong>+</strong> button in the tab bar. The <em>New request</em> menu lists every request type: HTTP, GraphQL, WebSocket, Socket.IO, Server-Sent Events, gRPC, MQTT and MCP. Choose <strong>GraphQL</strong>.</li><li>Paste <code>https://countries.trevorblades.com/</code> into the endpoint field.</li></ol>
<p>If you already have an HTTP request pointing at a GraphQL endpoint, you do not need to start over: the request-type picker next to the URL converts it in place, keeping the name, URL, headers, auth and scripts.</p>
<h2 id="step-2-load-the-schema">Step 2: load the schema</h2>
<p>With <strong>Introspect automatically when the URL is set</strong> switched on (it is in the request's <em>Schema</em> tab), the schema loads as soon as you enter a valid <code>http(s)://</code> URL. You can also press <strong>Fetch schema</strong> next to the URL. A notification tells you how many types were loaded, and the <em>Schema</em> tab shows the printed SDL.</p>
<p>Some production servers disable introspection. In that case switch <strong>Schema source</strong> to <strong>Manual (SDL or introspection JSON)</strong> and either paste the schema, use <strong>Import file</strong>, or <strong>Import from URL</strong> if your team publishes a <code>schema.graphql</code>. Press <strong>Apply schema</strong> and the editor behaves exactly as if introspection had worked. The manual schema is saved with the request, so teammates get it too.</p>
<h2 id="step-3-build-the-query">Step 3: build the query</h2>
<p>There are two ways to write the operation, and you can mix them.</p>
<p><strong>The explorer.</strong> Once a schema is loaded, an <strong>Explorer</strong> button appears above the query editor. It shows the root operation types (Query, Mutation, Subscription) as a tree. Ticking a field adds it to the operation; ticking an argument adds a variable for it and fills an example value into the variables editor. On fields that return a union or an interface, the explorer offers <code>... on Type</code> branches and keeps <code>__typename</code> in the selection so you can tell the results apart. The tree is virtualised, so large schemas with thousands of types stay responsive, and a filter box narrows the root fields.</p>
<p><strong>The editor.</strong> The query editor has schema-aware completion and validation. Type <code>{ country(code: &quot;IN&quot;) { </code> and it suggests the fields of <code>Country</code>. Misspelled fields are flagged before you send.</p>
<p>Try this query:</p>
<div class="code"><div class="code-head"><span>GraphQL</span></div><pre tabindex="0"><code class="language-graphql">query Country($code: ID!) {
  country(code: $code) {
    ...CountryBasics
    languages {
      code
      name
    }
  }
}

fragment CountryBasics on Country {
  code
  name
  capital
  currency
  emoji
}</code></pre></div>
<p>And in the <strong>Variables (JSON)</strong> editor underneath:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{ &quot;code&quot;: &quot;IN&quot; }</code></pre></div>
<p>Press <strong>Query</strong> (the button reads <strong>Mutate</strong> for a mutation and <strong>Subscribe</strong> for a subscription). The response panel shows the JSON result:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{
  &quot;data&quot;: {
    &quot;country&quot;: {
      &quot;code&quot;: &quot;IN&quot;,
      &quot;name&quot;: &quot;India&quot;,
      &quot;capital&quot;: &quot;New Delhi&quot;,
      &quot;currency&quot;: &quot;INR&quot;,
      &quot;emoji&quot;: &quot;🇮🇳&quot;,
      &quot;languages&quot;: [
        { &quot;code&quot;: &quot;hi&quot;, &quot;name&quot;: &quot;Hindi&quot; },
        { &quot;code&quot;: &quot;en&quot;, &quot;name&quot;: &quot;English&quot; }
      ]
    }
  }
}</code></pre></div>
<p>If a document contains several named operations, a selector appears next to the section tabs so you can pick which one to run; leaving it on <em>(first operation)</em> runs the first.</p>
<h3 id="use-environment-variables-inside-graphql-variables">Use environment variables inside GraphQL variables</h3>
<p>Variables in double braces work everywhere, including the GraphQL variables editor and the URL. Create an environment with <code>gqlUrl = https://countries.trevorblades.com/</code> and <code>country = IN</code>, then write:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{ &quot;code&quot;: &quot;{{country}}&quot; }</code></pre></div>
<p>Switching environment then switches the data without editing the request. The same works for staging and production endpoints: put <code>{{gqlUrl}}</code> in the URL field.</p>
<h2 id="step-4-headers-and-authentication">Step 4: headers and authentication</h2>
<p>Most real GraphQL APIs need a token. Use the request's <strong>Authorization</strong> tab rather than typing a header by hand: choose Bearer Token, API Key, Basic, OAuth 2.0 or any of the other schemes. Leave it on <strong>Inherit</strong> to use the auth configured on the folder or collection, so every operation in a collection shares one token. The same credentials are sent with the introspection call, which matters for servers that only allow introspection to authenticated users.</p>
<p>Anything else, such as a tenant header or <code>apollo-require-preflight</code>, goes in <strong>Headers</strong>.</p>
<p>If you need an OAuth 2.0 access token first, see <a href="https://swastik.codes/blog/oauth2-in-api-client/">OAuth 2.0 in an API client</a>.</p>
<h2 id="step-5-subscriptions">Step 5: subscriptions</h2>
<p>Subscriptions run over WebSocket. In the request's <strong>Settings</strong> tab you choose:</p>
<ul><li><strong>Subscription URL</strong> – leave it empty to use the endpoint URL with the scheme changed to <code>ws://</code> or <code>wss://</code>, or set a separate URL if your server listens elsewhere.</li><li><strong>Subscription protocol</strong> – <code>graphql-transport-ws</code> (the protocol used by the <code>graphql-ws</code> library and most current servers) or the legacy <code>graphql-ws</code> protocol from <code>subscriptions-transport-ws</code>.</li><li><strong>Connection params (JSON)</strong> – sent in the <code>connection_init</code> message; this is where many servers expect an auth token, for example <code>{ &quot;authorization&quot;: &quot;Bearer {{token}}&quot; }</code>.</li></ul>
<p>Write a <code>subscription { ... }</code> operation and press <strong>Subscribe</strong>. Events stream into the message log with timestamps until you press <strong>Unsubscribe</strong>. Very large payloads (over 1 MiB) are cut for display, so the log stays usable.</p>
<p>The Countries API has no subscriptions, so use your own server to try this. Remember that the server must be publicly reachable from the web app.</p>
<h2 id="step-6-test-the-response">Step 6: test the response</h2>
<p>A GraphQL server usually answers <code>200 OK</code> even when the operation failed: the problems are in an <code>errors</code> array. A status check alone therefore proves little. GraphQL requests have a <strong>Scripts</strong> tab with the same Pre-request and Post-response scripts as HTTP requests, using the Postman-style <code>pm.*</code> API. A useful baseline for the query above:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('HTTP 200', function () {
  swc.response.to.have.status(200);
});

const body = swc.response.json();

swc.test('no GraphQL errors', function () {
  swc.expect(body.errors, JSON.stringify(body.errors)).to.be.undefined;
});

swc.test('country has a capital and languages', function () {
  const c = body.data.country;
  swc.expect(c.capital).to.be.a('string').and.not.empty;
  swc.expect(c.languages).to.be.an('array').that.is.not.empty;
});

swc.test('response matches the expected shape', function () {
  swc.response.to.have.jsonSchema({
    type: 'object',
    required: ['data'],
    properties: {
      data: {
        type: 'object',
        required: ['country'],
        properties: {
          country: {
            type: 'object',
            required: ['code', 'name'],
            properties: { code: { type: 'string', minLength: 2, maxLength: 2 } }
          }
        }
      }
    }
  });
});</code></pre></div>
<p><code>jsonSchema</code> validates with Ajv inside the script sandbox. You can also save values for later requests, for example <code>swc.environment.set('countryName', body.data.country.name)</code>. The <strong>Test Results</strong> tab of the response shows each test as passed or failed. More patterns are in <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">writing API tests with pm scripts</a>.</p>
<p>Once the requests have tests, put them in a collection and run them together in the <a href="https://swastik.codes/features/collection-runner/">collection runner</a>, or on a schedule with a <a href="https://swastik.codes/features/api-monitoring/">monitor</a>.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<p><strong>&quot;Introspection failed&quot;.</strong> Check the URL first (many servers live at <code>/graphql</code>, not <code>/</code>). If the server needs auth for introspection, set it on the Authorization tab. If introspection is disabled in production, load the schema manually as described in step 2.</p>
<p><strong>The request never reaches my server.</strong> If the URL points at <code>localhost</code>, a <code>192.168.x.x</code> address or another private network, the swastik.codes server refuses it. Use the <a href="https://swastik.codes/download/">desktop app</a>, deploy to a reachable staging URL, or expose the service through a tunnel with a public hostname.</p>
<p><strong>HTTP 200 but no data.</strong> Look at the <code>errors</code> array in the response. Validation errors (unknown fields, missing required variables) come back this way on most servers.</p>
<p><strong>Variables are ignored.</strong> Variables must be valid JSON, and the operation must declare them (<code>query Country($code: ID!)</code>). Unused variables are not an error, but undeclared ones in the operation are.</p>
<p><strong>Subscription connects then closes.</strong> The usual causes are the wrong protocol (<code>graphql-transport-ws</code> versus legacy <code>graphql-ws</code>) or missing auth in the connection params.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="can-i-use-swastik-codes-as-a-free-online-graphql-client">Can I use Swastik Codes as a free online GraphQL client?</h3>
<p>Yes. The app is free to use today. You need a free account to send requests, because the web app sends them through the swastik.codes server.</p>
<h3 id="does-it-support-graphql-subscriptions">Does it support GraphQL subscriptions?</h3>
<p>Yes, over WebSocket with either the <code>graphql-transport-ws</code> protocol or the legacy <code>graphql-ws</code> protocol, with optional connection params.</p>
<h3 id="what-if-my-server-has-introspection-turned-off">What if my server has introspection turned off?</h3>
<p>Paste the SDL or an introspection JSON result into the Schema tab (or import it from a file or URL) and press Apply schema. Autocomplete, validation and the explorer then work from that schema.</p>
<h3 id="can-i-import-graphql-requests-from-postman-or-insomnia">Can I import GraphQL requests from Postman or Insomnia?</h3>
<p>Postman collections keep their GraphQL bodies (query and variables) when imported, and you can switch such a request to the GraphQL request type with the picker next to the URL to get the explorer and subscriptions. You can also import a GraphQL SDL file: it becomes a collection with one GraphQL request per query, mutation and subscription field, plus a <code>graphqlUrl</code> variable to point at your endpoint. See <a href="https://swastik.codes/blog/import-postman-collection/">importing a Postman collection</a> for the details and the <a href="https://swastik.codes/features/import-export/">import and export</a> page for all supported formats.</p>
<h3 id="can-i-generate-code-for-a-graphql-request">Can I generate code for a GraphQL request?</h3>
<p>Yes. A GraphQL request is an HTTP POST with a JSON body, so the code generator can turn it into cURL, JavaScript fetch, Python requests and many other snippets. See <a href="https://swastik.codes/features/code-generation/">code generation</a>.</p>
<h2 id="try-it">Try it</h2>
<p><a href="https://swastik.codes/app">Open the app</a>, add a GraphQL request for <code>https://countries.trevorblades.com/</code>, and you will have a schema-aware editor in under a minute. Sending needs a <a href="https://swastik.codes/signup">free account</a>. The <a href="https://swastik.codes/features/graphql-client/">GraphQL client feature page</a> summarises everything the client supports, and if your team also works with real-time APIs, the <a href="https://swastik.codes/features/websocket-client/">WebSocket client</a> uses the same workspace, environments and history.</p>]]></content:encoded>
</item>
<item>
<title>How to import a Postman collection into Swastik Codes (and export it back)</title>
<link>https://swastik.codes/blog/import-postman-collection/</link>
<guid isPermaLink="true">https://swastik.codes/blog/import-postman-collection/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>Postman</category><category>Import</category><category>Export</category>
<description>Export a Postman collection and environment as JSON, import them into Swastik Codes, fix common warnings, and export back to Postman or OpenAPI.</description>
<content:encoded><![CDATA[<p>To import a Postman collection into Swastik Codes, export it from Postman as <strong>Collection v2.1</strong> JSON, open <a href="https://swastik.codes/app">the app</a>, click <strong>Import</strong> in the top bar (or press Ctrl+O / Cmd+O), and drop the file in. The format is detected automatically, you get a preview of what will be created, and one click imports it. Environments and globals work the same way.</p>
<p>The rest of this guide covers each step in more detail, the other formats the same dialog accepts (Insomnia, OpenAPI, Swagger, cURL, HAR, WSDL, RAML, GraphQL), what the import notes mean, and how to export your work back out.</p>
<h2 id="step-1-export-from-postman">Step 1: export from Postman</h2>
<p>Postman lets you export each collection and environment as a JSON file. The exact menu wording changes between versions, but the flow has been stable for years:</p>
<ol><li>In Postman, open the <strong>Collections</strong> sidebar, open the menu (the three dots) next to the collection, and choose <strong>Export</strong>.</li><li>Pick <strong>Collection v2.1</strong> when you are asked for a format. Version 2.1 is the current, widely supported schema. Version 2.0 also imports fine.</li><li>Save the <code>.json</code> file.</li></ol>
<p>For environments, open the <strong>Environments</strong> sidebar, open the menu next to the environment, and choose <strong>Export</strong>. Globals can be exported from the globals view in the same way.</p>
<p>If you have many collections, Postman's account settings also offer a full <strong>data export</strong> (a &quot;data dump&quot;). It is a single JSON file containing your collections, environments and globals. Swastik Codes imports that file in one go.</p>
<aside class="callout callout-tip"><p><strong>Tip:</strong> If your collection uses the newer Postman Collection v3 format, or you keep a collection in a Git repository as a folder of YAML files, you can import those too (see &quot;Folders and v3&quot; below). They are converted by the swastik.codes server, so you need to be signed in.</p></aside>
<h2 id="step-2-open-the-import-dialog">Step 2: open the import dialog</h2>
<p><a href="https://swastik.codes/signup">Create a free account</a> and open <a href="https://swastik.codes/app">Swastik Codes</a>. Signing up creates a personal cloud workspace called &quot;My Workspace&quot;; import there to sync the collection across devices and share it with a team. To keep it on this device only, import into the <strong>Scratch Pad</strong> instead.</p>
<p>There are several ways into the same <strong>Import</strong> dialog:</p>
<ul><li>the <strong>Import</strong> button in the top bar;</li><li>the import icon at the top of the Collections sidebar;</li><li>the keyboard shortcut Ctrl+O (Cmd+O on macOS);</li><li>the command palette (Ctrl+K / Cmd+K), then &quot;Import…&quot;.</li></ul>
<p>The dialog has four tabs:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Tab</th><th>Use it for</th></tr></thead><tbody><tr><td><strong>File</strong></td><td>Drag one or more files in, or click <strong>Choose files</strong></td></tr><tr><td><strong>Folder</strong></td><td>Import every <code>.json</code>, <code>.yaml</code>, <code>.yml</code>, <code>.raml</code>, <code>.wsdl</code>, <code>.graphql</code> and <code>.har</code> file in a folder and its sub-folders</td></tr><tr><td><strong>Link</strong></td><td>Fetch a collection, OpenAPI/Swagger spec or HAR file from a URL</td></tr><tr><td><strong>Raw text</strong></td><td>Paste a cURL command, collection JSON or OpenAPI YAML</td></tr></tbody></table></div>
<p>The <strong>Link</strong> tab fetches the file from your browser, so the host has to allow cross-origin requests. Raw files on most code hosts and public spec URLs usually do.</p>
<h2 id="step-3-review-the-preview-and-import">Step 3: review the preview and import</h2>
<p>After you add a file, the dialog shows each source with a label for the format it detected: a Postman collection or environment, OpenAPI, HAR and so on. Below that is a list of everything that will be created: collections, environments and globals, each with a checkbox. By default everything is selected; untick anything you do not want and click <strong>Import</strong>.</p>
<p>If you imported a single request (a cURL command, for example), you also get <strong>Open as new request</strong>, which opens it in a tab without creating a collection.</p>
<p>The first imported collection opens in a tab straight away. Environments appear in the environment selector in the top bar.</p>
<h3 id="import-a-collection-and-its-environment-together">Import a collection and its environment together</h3>
<p>You can drop the collection file and the environment file in the same go. Both appear in the preview, and both are imported in one step. After importing, pick the environment in the selector and send a request to check variables resolve. Variables that resolve are highlighted, and missing ones are flagged in a different colour, so a typo in <code>{{baseUrl}}</code> is easy to spot.</p>
<p>You can also pin environments to a collection and choose a default one, so the right environment is selected whenever you open that collection's requests.</p>
<h2 id="step-4-read-the-import-notes">Step 4: read the import notes</h2>
<p>When something in the file cannot be represented exactly, the preview shows a <strong>Notes</strong> list. Nothing is silently dropped. Common notes:</p>
<ul><li><strong>&quot;<code>mocks</code> in the data dump are not imported&quot;</strong> (also <code>monitors</code>, <code>headerPresets</code>): a Postman data dump can contain these, but they are not imported. Recreate mocks and monitors from the imported collection.</li><li><strong>&quot;collection 3: …&quot;</strong> or <strong>&quot;environment 1: …&quot;</strong>: one item in a data dump could not be read. The rest of the dump is still imported.</li><li><strong>cURL notes</strong> such as &quot;proxy … was not imported; set it in the app's proxy settings&quot; or &quot;--cert was not imported; add certificates in settings&quot;: the option was understood, but it belongs in settings rather than in the request.</li><li><strong>OpenAPI notes</strong> such as &quot;unresolved $ref&quot; (only local <code>#/...</code> references are followed from a single file) or &quot;security scheme … is not defined&quot;.</li></ul>
<p>If a whole file fails, its row shows the error next to the file name and the other files can still be imported.</p>
<h2 id="other-formats-in-the-same-dialog">Other formats in the same dialog</h2>
<p>The same dialog accepts much more than Postman files. The format is sniffed from the content, so you do not have to choose it:</p>
<ul><li><strong>Postman</strong>: Collection v1, v2.0, v2.1 and v3, environments, globals and data dumps.</li><li><strong>Insomnia</strong>: v4 exports and v5 YAML. Workspaces become collections, request groups become folders, the base environment becomes collection variables, and Nunjucks references like <code>{{ _.host }}</code> become <code>{{host}}</code>. Insomnia scripts are kept with <code>insomnia.</code> changed to <code>swc.</code>. gRPC, WebSocket and Socket.IO requests become requests of those types.</li><li><strong>OpenAPI 3.x and Swagger 2.0</strong>, in JSON or YAML, from a file or a URL.</li><li><strong>cURL</strong> commands, including headers, data, form fields, basic auth and cookies.</li><li><strong>HAR 1.2</strong> files from browser dev tools. Every entry becomes a request, and its response becomes a saved example.</li><li><strong>WSDL 1.1 and 2.0</strong>: each SOAP operation becomes a POST request with a sample envelope and the right SOAP headers.</li><li><strong>RAML 0.8 and 1.0</strong>: resources become folders and methods become requests.</li><li><strong>GraphQL</strong> SDL and introspection results.</li></ul>
<p>RAML, WSDL and Postman v3 are converted by the server, so you need to be signed in for those.</p>
<h3 id="openapi-import-settings">OpenAPI import settings</h3>
<p>When the source is OpenAPI or Swagger, a <strong>View import settings</strong> toggle appears. These are the options:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Setting</th><th>What it does</th></tr></thead><tbody><tr><td>Naming requests</td><td>Use the summary/operationId, or always the URL</td></tr><tr><td>Folder organization</td><td>Group by tags, by paths, or no folders</td></tr><tr><td>Nested tag folders</td><td>An operation tagged <code>[a, b]</code> goes into folder a › b</td></tr><tr><td>Parameter generation</td><td>Fill values from examples, or from the schema as type placeholders</td></tr><tr><td>Enable optional parameters</td><td>Turn optional query and header parameters on (otherwise added but off)</td></tr><tr><td>Include deprecated</td><td>Import deprecated operations and parameters</td></tr><tr><td>Keep implicit headers</td><td>Keep <code>Accept</code>, <code>Content-Type</code> and <code>Authorization</code> header parameters</td></tr><tr><td>Always inherit authentication</td><td>Requests inherit auth from the collection</td></tr><tr><td>Auth in examples</td><td>Example requests carry the request's auth</td></tr><tr><td>Include webhooks</td><td>OpenAPI 3.1 webhooks become requests in a &quot;Webhooks&quot; folder</td></tr></tbody></table></div>
<p>Change a setting and click <strong>Apply settings</strong> to re-parse before importing. If you maintain the spec itself in Swastik Codes, the <a href="https://swastik.codes/features/api-design/">API design</a> tools can keep a generated collection in sync with it instead of re-importing.</p>
<h3 id="folders-and-v3">Folders and v3</h3>
<p>Choose the <strong>Folder</strong> tab to import a folder. This is useful for a repository of exported collections, and for Postman's Collection v3 folder layout (<code>*.request.yaml</code> files with a <code>.resources/definition.yaml</code>). Request kinds that have no equivalent are skipped with a note.</p>
<h2 id="step-5-export-back-out">Step 5: export back out</h2>
<p>Importing is not a one-way door. Open the menu next to a collection and choose <strong>Export</strong>, or use <strong>Export…</strong> in settings to export something else. You can export:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>What</th><th>Formats</th></tr></thead><tbody><tr><td>A collection</td><td>Collection v2.1 (recommended), Collection v2.0, Collection v3 (keeps gRPC, WebSocket, Socket.IO, MQTT and MCP requests), OpenAPI 3, or the Swastik Codes native format</td></tr><tr><td>An environment</td><td>Postman environment format</td></tr><tr><td>Globals</td><td>Postman format</td></tr><tr><td>History</td><td>HAR</td></tr><tr><td>Everything</td><td>A data dump</td></tr></tbody></table></div>
<p>The export dialog shows a preview of the file, which you can copy or download. A v2.1 export is the right choice for importing back into Postman or other tools that read Postman collections.</p>
<p>For keeping a workspace in Git, there is also a sync option that exports a workspace as a ZIP with one JSON file per collection and environment, and imports one back with a preview of the changes.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<p><strong>&quot;Nothing importable was found.&quot;</strong> The file is valid JSON or YAML but not a recognised format. Check you exported a collection rather than, for example, a run result.</p>
<p><strong>Variables show as missing after import.</strong> The collection expects an environment you have not imported, or the variable only had a current value in Postman. Current values are local to each device in Swastik Codes and are never stored on the server, so set an initial value for anything the team needs.</p>
<p><strong>The Link tab fails.</strong> The host does not allow cross-origin requests. Download the file and use the File tab instead.</p>
<p><strong>Requests import fine but fail to send.</strong> If they target <code>localhost</code> or a private IP, the web app cannot reach them because requests go through the swastik.codes server, which refuses private addresses. Use a publicly reachable URL.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="which-postman-collection-version-should-i-export">Which Postman collection version should I export?</h3>
<p>Collection v2.1. It is the current schema and the one most tools read. v2.0 and v1 also import, and v3 imports when you are signed in.</p>
<h3 id="do-i-need-an-account-to-import">Do I need an account to import?</h3>
<p>In the web app, yes: a free account (GitHub or email). Import into a cloud workspace to sync across devices and share with a team, or into the Scratch Pad to keep the collection in your browser only. The <a href="https://swastik.codes/download/">desktop app</a> imports without an account.</p>
<h3 id="are-my-pre-request-and-test-scripts-imported">Are my pre-request and test scripts imported?</h3>
<p>Yes, at collection, folder and request level. They run in a sandbox that implements the documented <code>pm.*</code> API, so they work as imported. <code>swc.*</code> is Swastik Codes' name for the same API; tick <strong>Convert Postman scripts to swc.*</strong> when importing to rewrite them, and exporting to Postman writes <code>pm.*</code> back. See <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">Writing API tests with swc.* scripts</a>.</p>
<h3 id="can-i-import-an-openapi-spec-straight-from-a-url">Can I import an OpenAPI spec straight from a URL?</h3>
<p>Yes, on the Link tab, as long as the host allows cross-origin requests.</p>
<h2 id="next-steps">Next steps</h2>
<p><a href="https://swastik.codes/app">Open the app</a> and import a collection now, then <a href="https://swastik.codes/signup">create a free account</a> to send requests. The <a href="https://swastik.codes/features/import-export/">import and export feature page</a> lists every format, and <a href="https://swastik.codes/blog/migrate-from-postman/">Moving from Postman</a> covers what to check once your collections are across. When you are ready to run the whole collection with a CSV of test data, read <a href="https://swastik.codes/blog/data-driven-api-testing/">data-driven API testing</a>.</p>]]></content:encoded>
</item>
<item>
<title>A free mock API server built from your saved examples</title>
<link>https://swastik.codes/blog/free-mock-api-server/</link>
<guid isPermaLink="true">https://swastik.codes/blog/free-mock-api-server/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>Mock server</category><category>REST</category><category>Testing</category>
<description>Create a free mock REST API online from saved examples: matching rules, x-mock-* headers, dynamic templates, private mocks and a call log.</description>
<content:encoded><![CDATA[<p>A mock API server answers HTTP requests with canned responses, so a frontend, a mobile app or a test suite can work before the real backend exists, or while it is down. In Swastik Codes, a mock server is built from the <strong>saved examples</strong> in a collection: each example is a response for a request, and the mock serves the best matching one at a public URL such as <code>https://swastik.codes/api/mock/&lt;mock-id&gt;/users/42</code>.</p>
<p>Mock servers are free to use. This guide shows how to create one, how requests are matched to examples, how to force a specific response with <code>x-mock-*</code> headers, how to make responses dynamic with templates, and how to keep a mock private.</p>
<h2 id="how-it-works-in-one-paragraph">How it works in one paragraph</h2>
<p>You have a collection with requests such as <code>GET {{baseUrl}}/users/:id</code>. Each request can have saved examples: a status code, headers and a body, plus the request that produced them. When a call arrives at the mock URL, the server looks for requests with the same method and path (<code>:id</code> and <code>{{variables}}</code> in the path act as wildcards), scores their examples on query parameters, headers and body, and returns the best one. If nothing matches, it answers <code>404</code> with the error code <code>mock_request_not_found</code>.</p>
<h2 id="step-1-create-a-collection-with-examples">Step 1: create a collection with examples</h2>
<p><a href="https://swastik.codes/app">Open the app</a> and <a href="https://swastik.codes/signup">create a free account</a>. Mock servers with a public URL live in cloud workspaces, so you need to be signed in. (You can also create a <em>local</em> mock that lives on your device and answers only inside the app, which is enough to try the matching rules.)</p>
<p>Create a collection called &quot;Users API&quot; and add a request:</p>
<div class="code"><pre tabindex="0"><code>GET {{baseUrl}}/users/:id</code></pre></div>
<p>There are two ways to give it an example:</p>
<ol><li><strong>From a real response.</strong> Send the request to a real API and click <strong>Save as example</strong> above the response. The status, headers and body are saved.</li><li><strong>By hand.</strong> Once the mock exists, its <strong>Examples</strong> tab lists every request in the collection. Click <strong>Add example</strong> next to a request and fill in the name, status code, status text, response headers and body.</li></ol>
<p>Here is an example body for a <code>200 OK</code> response:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{
  &quot;id&quot;: 42,
  &quot;name&quot;: &quot;Asha Rao&quot;,
  &quot;email&quot;: &quot;asha@example.com&quot;,
  &quot;plan&quot;: &quot;team&quot;
}</code></pre></div>
<p>Add a second example to the same request with status <code>404</code> and a body like <code>{&quot;error&quot;: &quot;user not found&quot;}</code>. Having both lets you test the &quot;not found&quot; path of your client later.</p>
<p>You can also import an existing collection. Postman collections with saved examples, HAR files (every captured response becomes an example) and OpenAPI specs with examples all give you a mock-ready collection in one step. See <a href="https://swastik.codes/blog/import-postman-collection/">How to import a Postman collection</a>.</p>
<h2 id="step-2-create-the-mock-server">Step 2: create the mock server</h2>
<p>Click <strong>New</strong> in the top bar and choose <strong>New mock server</strong>. The dialog asks for:</p>
<ul><li><strong>Collection</strong>: the collection whose examples will be served. The dialog previews the routes that will be served, with the number of examples and status codes for each, and lists requests that have no examples (and so will not be served).</li><li><strong>Name</strong>: defaults to &quot;&lt;collection&gt; mock&quot;.</li><li><strong>Environment</strong>: optional. Its variables resolve <code>{{variables}}</code> inside examples.</li><li><strong>Simulated network delay</strong>: none, 300 ms, 800 ms, 2 s or a custom value.</li><li><strong>Private</strong>: callers must send an <code>x-api-key</code> header with one of your API keys.</li></ul>
<p>After you create it, the mock's <strong>Overview</strong> tab shows its URL with a <strong>Copy</strong> button and a ready-made <code>curl</code> command.</p>
<h2 id="step-3-call-it">Step 3: call it</h2>
<p>The mock URL is the root of your API. Append the path that the request uses after its base URL:</p>
<div class="code"><div class="code-head"><span>Shell</span></div><pre tabindex="0"><code class="language-bash">curl -i https://swastik.codes/api/mock/&lt;mock-id&gt;/users/42</code></pre></div>
<p>The path in the saved request is <code>{{baseUrl}}/users/:id</code>, so <code>/users/42</code>, <code>/users/7</code> and <code>/users/abc</code> all match. You get the <code>200</code> example back, with the status and headers you saved.</p>
<p>The mock answers CORS requests from any origin, so you can call it from a frontend running on <code>localhost:5173</code> without any proxy:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const res = await fetch(
  &quot;https://swastik.codes/api/mock/&lt;mock-id&gt;/users/42&quot;
);
console.log(res.status, await res.json());</code></pre></div>
<p>Request bodies sent to a mock can be up to 1 MiB.</p>
<h2 id="choosing-a-response-with-x-mock-headers">Choosing a response with x-mock-* headers</h2>
<p>When a request has several examples, the scoring picks the closest one. When you want a specific response, for example to test how your app handles errors, send one of these headers:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Header</th><th>Effect</th></tr></thead><tbody><tr><td><code>x-mock-response-name: Not found</code></td><td>Use the example with this name</td></tr><tr><td><code>x-mock-response-code: 404</code></td><td>Use an example with this status code</td></tr><tr><td><code>x-mock-response-id: &lt;example id&gt;</code></td><td>Use this exact example</td></tr><tr><td><code>x-mock-response-delay: 1500</code></td><td>Wait this many milliseconds before answering (0 to 30,000)</td></tr><tr><td><code>x-mock-match-request-body: true</code></td><td>Only examples whose saved request body equals this call's body can answer</td></tr><tr><td><code>x-mock-match-request-headers: x-tenant, accept</code></td><td>Only examples whose saved request has these headers with the same values can answer (<code>true</code> = every header the example's request carries)</td></tr></tbody></table></div>
<div class="code"><div class="code-head"><span>Shell</span></div><pre tabindex="0"><code class="language-bash">curl -i https://swastik.codes/api/mock/&lt;mock-id&gt;/users/42 \
  -H &quot;x-mock-response-code: 404&quot;</code></pre></div>
<p>The <code>x-mock-*</code> headers themselves never take part in matching.</p>
<h3 id="strict-matching-as-a-setting">Strict matching as a setting</h3>
<p>Body and header matching can also be switched on for the whole mock in its <strong>Overview</strong> tab under <strong>Response matching</strong>: tick <strong>Match request body</strong> and/or <strong>Match request headers</strong> and list the header names. JSON bodies are compared structurally, so key order and whitespace do not matter. This is useful for a <code>POST /login</code> that should return <code>200</code> for the right credentials and <code>401</code> for anything else: save one example per case and turn on body matching.</p>
<h2 id="dynamic-responses-with-templates">Dynamic responses with templates</h2>
<p>A static example gets you far, but sometimes the response should echo the request. Example bodies and header values support template helpers that read from the incoming call:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Helper</th><th>Reads</th></tr></thead><tbody><tr><td><code>{{$body 'user.name' 'default'}}</code></td><td>A value from the JSON request body (lodash-style path such as <code>items[0].sku</code>), or a form field</td></tr><tr><td><code>{{$queryParams 'page' '1'}}</code></td><td>A query parameter</td></tr><tr><td><code>{{$pathSegments '1' ''}}</code></td><td>A path segment, counting from 0 after the mock root</td></tr><tr><td><code>{{$headers 'x-request-id' ''}}</code></td><td>A request header (case-insensitive)</td></tr></tbody></table></div>
<p>The second argument is the default when the value is missing. Strings are inserted as plain text (so put quotes around them in a JSON body), numbers and booleans as written, and objects and arrays as JSON.</p>
<p>For <code>GET /users/:id</code>, this example body returns whatever id was asked for:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{
  &quot;id&quot;: &quot;{{$pathSegments '1'}}&quot;,
  &quot;name&quot;: &quot;Asha Rao&quot;,
  &quot;requestedAt&quot;: &quot;{{$timestamp}}&quot;
}</code></pre></div>
<p><code>/users/42</code> gives <code>&quot;id&quot;: &quot;42&quot;</code>. Path segments are counted after the mock root, so segment <code>0</code> is <code>users</code> and segment <code>1</code> is the id.</p>
<p>For a <code>POST /orders</code> that should echo what was sent:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{
  &quot;orderId&quot;: &quot;{{$guid}}&quot;,
  &quot;sku&quot;: &quot;{{$body 'items[0].sku' 'unknown'}}&quot;,
  &quot;quantity&quot;: {{$body 'items[0].quantity' '1'}},
  &quot;status&quot;: &quot;received&quot;
}</code></pre></div>
<p>Two kinds of substitution happen, in order. First <code>{{variables}}</code> are resolved from the collection variables and the mock's environment, together with dynamic variables such as <code>{{$guid}}</code>, <code>{{$timestamp}}</code> and <code>{{$randomInt}}</code>. Then the request helpers run. Because helpers run last, text copied from the incoming request is never itself treated as a variable.</p>
<h2 id="private-mocks">Private mocks</h2>
<p>By default anyone with the URL can call a mock. Tick <strong>Private</strong> to require an API key: callers send <code>x-api-key: swc_...</code> with a key belonging to someone who has access to the workspace. Calls without a valid key get <code>401</code> or <code>403</code>. Create keys in your account settings; each key is shown once when created.</p>
<div class="code"><div class="code-head"><span>Shell</span></div><pre tabindex="0"><code class="language-bash">curl https://swastik.codes/api/mock/&lt;mock-id&gt;/users/42 \
  -H &quot;x-api-key: $SWC_API_KEY&quot;</code></pre></div>
<h2 id="the-call-log">The call log</h2>
<p>The <strong>Call log</strong> tab shows the last 100 calls to the mock: method, path, query, status, which example answered, request headers, request body and how long it took. You can filter by path, method and status. Sensitive headers (<code>Authorization</code>, <code>Cookie</code>, <code>X-Api-Key</code> and <code>Proxy-Authorization</code>) are stored as <code>[redacted]</code>.</p>
<p>The call log is the quickest way to debug &quot;why did I get a 404&quot;: you can see the exact path and headers the mock received and compare them with the saved request.</p>
<p>The <strong>Try it</strong> tab sends calls to the mock from inside the app, which is handy when you want to check a template without leaving the browser.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<p><strong>404 <code>mock_request_not_found</code>.</strong> No request in the collection has the same method and path, or strict matching filtered every example out. Check the call log for the exact path, and check that the request has at least one example. Remember that the path is matched after the base URL: for <code>{{baseUrl}}/v1/users</code>, call <code>/v1/users</code> on the mock.</p>
<p><strong>The wrong example answers.</strong> Add <code>x-mock-response-name</code> or <code>x-mock-response-code</code>, or give the examples' saved requests distinguishing query parameters or headers so scoring can tell them apart.</p>
<p><strong><code>{{baseUrl}}</code> appears in the response.</strong> The example uses a variable that is defined neither in the collection nor in the mock's environment. Pick an environment in the mock settings or add a collection variable.</p>
<p><strong>A template prints the default.</strong> The path, field or header name does not exist in the incoming request. Header names are case-insensitive; JSON paths are case-sensitive.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="is-the-mock-server-free">Is the mock server free?</h3>
<p>Yes. Swastik Codes is free to use today, including mock servers.</p>
<h3 id="can-i-mock-an-api-from-an-openapi-spec">Can I mock an API from an OpenAPI spec?</h3>
<p>Yes. Import the spec (or design it with the <a href="https://swastik.codes/features/api-design/">API design</a> tools) to generate a collection; operations that have examples in the spec get saved examples you can serve.</p>
<h3 id="does-the-mock-support-graphql-or-websocket">Does the mock support GraphQL or WebSocket?</h3>
<p>The mock server answers HTTP requests from saved examples. A GraphQL query sent as an HTTP <code>POST</code> can be mocked like any other request, using body matching to tell queries apart. WebSocket and other streaming protocols are not mocked.</p>
<h3 id="can-i-use-the-mock-from-a-browser-app-on-localhost">Can I use the mock from a browser app on localhost?</h3>
<p>Yes. Mocks allow requests from any origin, so a local frontend can call the mock URL directly.</p>
<h3 id="how-many-calls-are-logged">How many calls are logged?</h3>
<p>The last 100 calls per mock, with secrets redacted.</p>
<h2 id="start-mocking">Start mocking</h2>
<p><a href="https://swastik.codes/app">Open the app</a>, <a href="https://swastik.codes/signup">create a free account</a>, save a couple of examples and click <strong>New mock server</strong>. The <a href="https://swastik.codes/features/mock-server/">mock server feature page</a> summarises everything above, and once the real API exists you can point the same collection at it and check it with <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">pm.* tests</a>.</p>]]></content:encoded>
</item>
<item>
<title>How to test a Socket.IO server online (v2, v3 and v4)</title>
<link>https://swastik.codes/blog/test-socket-io-server/</link>
<guid isPermaLink="true">https://swastik.codes/blog/test-socket-io-server/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>Socket.IO</category><category>WebSocket</category><category>Testing</category>
<description>Test a Socket.IO server from your browser: connect to namespaces, send auth, emit events with acks and listen for server events. Works with v2, v3 and v4.</description>
<content:encoded><![CDATA[<p>To test a Socket.IO server you need a client that speaks the Socket.IO protocol, not just raw WebSocket. Socket.IO wraps its messages in its own packet format, runs a handshake, multiplexes namespaces over one connection and supports acknowledgements, so a plain WebSocket tool will connect and then see nothing useful. In Swastik Codes you create a <strong>Socket.IO</strong> request, pick the client version (v4, v3 or v2), set the namespace and auth payload, press <strong>Connect</strong>, and then emit events and watch incoming ones in the log.</p>
<p>This guide shows the full flow against a small server you can deploy yourself, explains the version differences that trip people up, and lists the errors you are most likely to see.</p>
<h2 id="what-you-need">What you need</h2>
<ul><li>A Socket.IO server reachable from the public internet. The Swastik Codes web app relays the connection through the swastik.codes server, which refuses private, loopback and other reserved addresses. A server on <code>localhost:3000</code> cannot be reached from the web app, so deploy it to a staging host or put it behind a tunnel with a public hostname. The <a href="https://swastik.codes/download/">desktop app</a> for Windows and Linux connects from your own machine, so it reaches local servers.</li><li>To know which Socket.IO major version the server runs. Check <code>socket.io</code> in its <code>package.json</code> (or the equivalent library in Python, Java or Go).</li><li>A free Swastik Codes account, because protocol connections run through the server.</li></ul>
<h2 id="socket-io-versions-in-one-paragraph">Socket.IO versions in one paragraph</h2>
<p>Socket.IO v3 and v4 share the same wire protocol (Socket.IO protocol 5 over Engine.IO 4), and a v4 client can talk to a v3 server. Socket.IO v2 uses the older protocol 4 over Engine.IO 3 and is not compatible with v3/v4 clients: the handshake query says <code>EIO=3</code> instead of <code>EIO=4</code>, the namespace connection is different, and v2 has no handshake <code>auth</code> payload (credentials usually go in the query string). If your connection opens and then nothing happens, a version mismatch is the first thing to check. v4 servers can accept v2 clients only when they are started with <code>allowEIO3: true</code>.</p>
<h2 id="step-1-create-a-socket-io-request">Step 1: create a Socket.IO request</h2>
<ol><li>Open <a href="https://swastik.codes/app">the app</a> and sign in.</li><li>Click <strong>+</strong> in the tab bar and choose <strong>Socket.IO</strong> from the <strong>New request</strong> menu.</li><li>Enter the server URL, for example <code>https://realtime.example.com</code>. Use the <code>http(s)://</code> form of the URL; Socket.IO starts its handshake over HTTP.</li><li>Choose the <strong>Client version</strong>: v4, v3 or v2.</li></ol>
<p>On the <strong>Settings</strong> tab you can also set:</p>
<ul><li><strong>Handshake path</strong>: the server's <code>path</code> option, <code>/socket.io</code> by default. Change it if your server mounts Socket.IO elsewhere, for example <code>/ws/socket.io</code> behind a proxy.</li><li><strong>Namespace</strong>: <code>/</code> by default, or for example <code>/admin</code> or <code>/chat</code>.</li><li><strong>Transport</strong>: <strong>WebSocket</strong> or <strong>HTTP long-polling</strong>. With WebSocket selected, the client falls back to long-polling if the WebSocket transport fails, and the log says so.</li><li><strong>Auth payload (JSON, v3/v4 handshake)</strong>: the object your server reads as <code>socket.handshake.auth</code>.</li><li>Timeouts for the handshake and for acknowledgements, automatic reconnection, the maximum message size and TLS certificate verification.</li></ul>
<p>The <strong>Params</strong>, <strong>Headers</strong> and <strong>Authorization</strong> tabs work like they do for HTTP requests. Query parameters end up in <code>socket.handshake.query</code> on the server, headers in <code>socket.handshake.headers</code>, and the authorization types (Bearer, Basic, API key, OAuth 2.0) are sent as handshake headers or query parameters.</p>
<h2 id="step-2-listen-for-events">Step 2: listen for events</h2>
<p>Open the <strong>Events</strong> tab and add the event names you care about, such as <code>chat:message</code> or <code>order:updated</code>. Incoming events with those names are highlighted in the log. Turn on <strong>Show only listened events</strong> to hide everything else, which helps on busy servers. You can add a description to each listener to document what the event carries, and switch individual listeners on and off.</p>
<p>All events the server sends are still received and shown unless you filter them, so you will not miss an event just because you forgot to add a listener.</p>
<h2 id="step-3-connect">Step 3: connect</h2>
<p>Press <strong>Connect</strong>. The log shows the connection opening, the namespace being joined and any events the server emits on connect. <strong>Connection details</strong> shows the handshake request and response headers.</p>
<p>If the server rejects the connection in middleware (<code>next(new Error('unauthorized'))</code>), you will see an error event with the message the server sent, followed by a close. That message is exactly what your real clients would receive in their <code>connect_error</code> handler, which makes this a quick way to test your auth middleware.</p>
<h2 id="step-4-emit-events-and-use-acknowledgements">Step 4: emit events and use acknowledgements</h2>
<p>On the <strong>Message</strong> tab, enter an <strong>Event</strong> name and the <strong>Arguments</strong>. Arguments can be written in two modes:</p>
<ul><li><strong>JSON array</strong>: one array where each element is an argument, for example <code>[{&quot;room&quot;: &quot;lobby&quot;, &quot;text&quot;: &quot;hello&quot;}]</code> for a single object argument, or <code>[&quot;lobby&quot;, &quot;hello&quot;]</code> for two string arguments.</li><li><strong>One per argument</strong>: a separate editor per argument with its own format (JSON, text, number, boolean or binary). Binary arguments are sent as Socket.IO attachments.</li></ul>
<p>Turn on <strong>Ack</strong> to ask the server for an acknowledgement. The client then waits for the server to call the callback and shows the ack's arguments in the log. If no ack arrives within the ack timeout, you see a timeout error; set the timeout to 0 to wait forever.</p>
<p>Acks also work the other way. If the server emits an event with a callback and expects the client to acknowledge it, Swastik Codes answers the acknowledgement automatically (with no arguments), so server code that waits for an ack does not hang during your test.</p>
<p>Press <strong>Send</strong>. The emitted event appears in the log as sent, with its namespace and ack id, followed by whatever the server sends back.</p>
<h2 id="step-5-save-events-and-examples">Step 5: save events and examples</h2>
<p>Save the events you emit often so you can send them again with one click from the <strong>Saved</strong> tab while connected. Each saved message remembers its event name and arguments. When the connection is closed, <strong>Save as example</strong> stores the messages and status of the session as an example of the request. Save the request to a collection with Ctrl+S or Cmd+S to share it with your workspace.</p>
<h2 id="a-socket-io-v4-server-to-test-against">A Socket.IO v4 server to test against</h2>
<p>Here is a small Socket.IO v4 server with auth middleware, an event with an acknowledgement, a broadcast and a second namespace. It covers every feature discussed above.</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-js">// server.mjs  (npm install socket.io)
import { createServer } from 'node:http';
import { Server } from 'socket.io';

const httpServer = createServer();
const io = new Server(httpServer, {
  cors: { origin: '*' },
});

// Reject connections without the right token in the auth payload.
io.use((socket, next) =&gt; {
  if (socket.handshake.auth?.token !== process.env.SIO_TOKEN) {
    return next(new Error('unauthorized'));
  }
  next();
});

io.on('connection', (socket) =&gt; {
  socket.emit('welcome', { id: socket.id, at: Date.now() });

  socket.on('chat:message', (msg, ack) =&gt; {
    if (!msg || typeof msg.text !== 'string') {
      return typeof ack === 'function' &amp;&amp; ack({ ok: false, error: 'text is required' });
    }
    io.emit('chat:message', { from: socket.id, text: msg.text });
    if (typeof ack === 'function') ack({ ok: true });
  });

  // Ask the client for an ack; Swastik Codes answers it automatically.
  socket.timeout(5000).emit('server:ping', (err) =&gt; {
    console.log(err ? 'client did not ack' : 'client acked');
  });
});

io.of('/admin').on('connection', (socket) =&gt; {
  socket.emit('stats', { clients: io.engine.clientsCount });
});

httpServer.listen(Number(process.env.PORT) || 3000);</code></pre></div>
<p>Deploy it, set <code>SIO_TOKEN</code>, then in Swastik Codes:</p>
<ol><li>URL <code>https://your-host</code>, <strong>Client version</strong> v4, <strong>Auth payload</strong> <code>{&quot;token&quot;: &quot;{{sioToken}}&quot;}</code>.</li><li>Add listeners for <code>welcome</code> and <code>chat:message</code>.</li><li>Connect. You should see <code>welcome</code> and the <code>server:ping</code> event; your server logs &quot;client acked&quot;.</li><li>Emit <code>chat:message</code> with arguments <code>[{&quot;text&quot;: &quot;hello&quot;}]</code> and <strong>Ack</strong> on. The log shows the broadcast <code>chat:message</code> and the ack <code>{&quot;ok&quot;: true}</code>.</li><li>Emit <code>chat:message</code> with <code>[{}]</code> to check the error path: the ack carries <code>{&quot;ok&quot;: false, &quot;error&quot;: &quot;text is required&quot;}</code>.</li><li>Change the namespace to <code>/admin</code> and reconnect to see the <code>stats</code> event.</li></ol>
<p>Then remove the token from the auth payload and connect again to confirm that the middleware rejects you with <code>unauthorized</code>.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<p><strong>Connected, but no events and no errors.</strong> Most often a version mismatch (a v2 server with a v3/v4 client, or the other way round). Switch the <strong>Client version</strong>. Also check that you are in the right namespace.</p>
<p><strong><code>Invalid namespace</code> error.</strong> The namespace does not exist on the server. Namespaces are case-sensitive and start with <code>/</code>.</p>
<p><strong>404 during the handshake.</strong> The <strong>Handshake path</strong> does not match the server's <code>path</code> option, or a reverse proxy does not forward <code>/socket.io/</code> to the Node process.</p>
<p><strong>WebSocket transport fails, polling works.</strong> A proxy is not forwarding the <code>Upgrade</code> header. The client falls back to long-polling so you can keep testing, but fix the proxy before production: polling uses more requests and adds latency.</p>
<p><strong>Ack timeout.</strong> The server handler does not call the callback, or calls it only on some code paths. Raise the ack timeout to rule out a slow handler.</p>
<p><strong>Connection refused for a private address.</strong> The relay blocks private and loopback addresses on purpose. Use a public hostname.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="can-i-use-a-plain-websocket-client-to-test-socket-io">Can I use a plain WebSocket client to test Socket.IO?</h3>
<p>Only in a very limited way. You would have to write Engine.IO and Socket.IO packets by hand (<code>40</code> to join the default namespace, <code>42[&quot;event&quot;, …]</code> to emit). The Socket.IO request type does that for you, along with namespaces, acks and reconnection.</p>
<h3 id="which-socket-io-versions-are-supported">Which Socket.IO versions are supported?</h3>
<p>Client versions v2, v3 and v4. Pick the one that matches your server; v4 also works with v3 servers.</p>
<h3 id="how-do-i-send-a-token-to-a-socket-io-server">How do I send a token to a Socket.IO server?</h3>
<p>For v3 and v4 servers, put it in the <strong>Auth payload</strong> JSON, which the server reads as <code>socket.handshake.auth</code>. You can also send it as a query parameter or a header. v2 has no auth payload, so use the query string.</p>
<h3 id="can-i-send-binary-data">Can I send binary data?</h3>
<p>Yes. In <strong>One per argument</strong> mode, choose the binary format for an argument and it is sent as a Socket.IO binary attachment.</p>
<h3 id="does-it-answer-acknowledgements-the-server-asks-for">Does it answer acknowledgements the server asks for?</h3>
<p>Yes. When the server emits with a callback, the client acknowledges automatically, so server code waiting for the ack continues.</p>
<h2 id="try-it">Try it</h2>
<p>Open <a href="https://swastik.codes/app">Swastik Codes</a>, create a Socket.IO request and connect to your server. You need a <a href="https://swastik.codes/signup">free account</a> to connect. The <a href="https://swastik.codes/features/socket-io-client/">Socket.IO client feature page</a> lists every option. If you also run raw WebSocket endpoints, read <a href="https://swastik.codes/blog/test-websocket-api-online/">how to test a WebSocket API online</a>, and for one-way streams see <a href="https://swastik.codes/blog/test-server-sent-events/">how to test Server-Sent Events</a>.</p>]]></content:encoded>
</item>
<item>
<title>How to test a WebSocket API online, step by step</title>
<link>https://swastik.codes/blog/test-websocket-api-online/</link>
<guid isPermaLink="true">https://swastik.codes/blog/test-websocket-api-online/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>WebSocket</category><category>Testing</category><category>Realtime</category>
<description>Connect to a WebSocket endpoint from your browser, send text, JSON or binary frames, save messages and debug handshakes with Swastik Codes.</description>
<content:encoded><![CDATA[<p>To test a WebSocket API online, you need a client that can open a <code>ws://</code> or <code>wss://</code> connection, send frames you write, and show every frame the server sends back with timestamps. In Swastik Codes that takes about a minute: open the app, create a <strong>WebSocket</strong> request, paste the URL, press <strong>Connect</strong>, type a message and press <strong>Send</strong>. The rest of this guide walks through that flow in detail, explains what happens during the handshake, and covers the problems you are most likely to hit.</p>
<h2 id="what-you-need">What you need</h2>
<ul><li>A WebSocket endpoint that is reachable from the public internet, for example <code>wss://api.example.com/socket</code>. If you only want to try the client first, <code>wss://echo.websocket.org</code> is a public echo server: it sends one greeting line when you connect and then returns every message you send.</li><li>A free Swastik Codes account. The web app needs one, because traffic goes through the server (see below for why).</li><li>Whatever credentials your API expects during the handshake: a bearer token, an API key, a cookie or a query parameter.</li></ul>
<h3 id="why-the-endpoint-must-be-public">Why the endpoint must be public</h3>
<p>A browser page cannot open arbitrary WebSocket connections with custom headers, and it cannot reach servers that block cross-origin requests. So the Swastik Codes web app does not connect to your endpoint directly. It opens one WebSocket to the swastik.codes server, and the server opens the real connection to your endpoint and relays frames in both directions.</p>
<p>That relay protects itself and everybody else: it refuses private, loopback, link-local and other reserved addresses. <code>ws://localhost:8080</code> or <code>ws://192.168.1.20</code> will not work from the web app. Deploy your server somewhere public (a staging environment, a small VM, a platform such as Fly.io or Render), or use a tunnelling tool that gives you a public HTTPS hostname. The <a href="https://swastik.codes/download/">desktop app</a> for Windows and Linux runs connections from your own machine, so it reaches local servers.</p>
<h2 id="step-1-create-a-websocket-request">Step 1: create a WebSocket request</h2>
<ol><li>Open <a href="https://swastik.codes/app">the app</a> and sign in.</li><li>Click the <strong>+</strong> button at the end of the tab bar. The <strong>New request</strong> menu lists every request type: HTTP, GraphQL, WebSocket, Socket.IO, Server-Sent Events, gRPC, MQTT and MCP. Choose <strong>WebSocket</strong>.</li><li>Paste your URL into the address bar, for example <code>wss://echo.websocket.org</code>.</li></ol>
<p>If you already have an HTTP request pointing at the same host, you can change its type in place with the request type picker. The URL scheme is converted for you (<code>https://</code> becomes <code>wss://</code>), and the name, headers, query parameters and authorization carry over.</p>
<h2 id="step-2-add-headers-query-parameters-and-auth">Step 2: add headers, query parameters and auth</h2>
<p>Most production WebSocket APIs authenticate during the opening handshake, because after that the connection is just a stream of frames. The request editor has the same tabs you know from HTTP requests:</p>
<ul><li><strong>Params</strong>: query parameters appended to the URL, such as <code>?token=…</code> or <code>?room=lobby</code>.</li><li><strong>Authorization</strong>: Bearer token, Basic auth, API key (as a header or a query parameter) or OAuth 2.0. The choice is turned into handshake headers or query parameters. You can also inherit auth from the collection the request is saved in.</li><li><strong>Headers</strong>: any extra handshake headers, for example <code>Origin</code>, a custom <code>X-Client-Version</code> or a <code>Cookie</code>.</li><li><strong>Settings</strong>: subprotocols (sent as <code>Sec-WebSocket-Protocol</code>), ping interval, handshake request timeout, maximum message size, automatic reconnection with attempts and interval, and whether to verify the server's TLS certificate.</li></ul>
<p>Values can use variables like <code>{{baseUrl}}</code> and <code>{{token}}</code> from your environments, so the same request works against staging and production.</p>
<h2 id="step-3-connect-and-read-the-handshake">Step 3: connect and read the handshake</h2>
<p>Press <strong>Connect</strong>. The status changes to connecting and then to connected. The message log on the right shows the connection events as well as messages. Open <strong>Connection details</strong> to see the request headers that were sent and the response headers the server returned. This is where you check things like the negotiated subprotocol or a <code>Set-Cookie</code> from the upgrade response.</p>
<p>When a handshake fails, the log shows an error event followed by a close event, with the reason. A <code>401</code> or <code>403</code> here almost always means the token or origin was rejected; a <code>404</code> means the path is wrong; a <code>426 Upgrade Required</code> or a plain <code>200</code> usually means you hit an HTTP endpoint that does not speak WebSocket at all.</p>
<h2 id="step-4-send-messages">Step 4: send messages</h2>
<p>The <strong>Message</strong> tab is a composer. Pick a format first:</p>
<ul><li><strong>Text</strong> sends a text frame exactly as typed.</li><li><strong>JSON</strong>, <strong>XML</strong> and <strong>HTML</strong> also send text frames, with syntax highlighting and a <strong>Beautify</strong> button.</li><li><strong>Binary</strong> sends a binary frame. Write the bytes as Base64 or hexadecimal and choose the encoding next to the format.</li></ul>
<p>Type a message such as:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{ &quot;type&quot;: &quot;subscribe&quot;, &quot;channel&quot;: &quot;orders&quot;, &quot;id&quot;: 1 }</code></pre></div>
<p>and press <strong>Send</strong>. Variables in the message are resolved before it is sent, so <code>{&quot;token&quot;: &quot;{{token}}&quot;}</code> works. The <strong>Ping</strong> button sends a WebSocket ping frame, which is useful for checking that a quiet connection is still alive.</p>
<p>Every sent and received frame appears in the log with its direction, time and size. You can filter by sent or received, search the log, and view a message as text, JSON or a hexdump. Select two messages to see the time between them, which is a quick way to measure how long your server takes to answer a particular request. <strong>Download the shown messages as JSON</strong> exports the visible log if you want to attach it to a bug report.</p>
<h2 id="step-5-save-messages-and-examples">Step 5: save messages and examples</h2>
<p>Testing a WebSocket API usually means sending the same handful of messages over and over: authenticate, subscribe, send a command, unsubscribe. Instead of keeping them in a text file:</p>
<ul><li>Click <strong>Save message</strong> in the composer to keep the current message with the request. Saved messages appear under <strong>Saved messages</strong>, where you can rename them, load one back into the composer to edit it, delete it, or send it with one click while connected.</li><li>Use <strong>Save as example</strong> on the log once the connection is closed. It stores the messages, status and metadata of that session as an example of the request, which is handy as documentation of what a typical exchange looks like.</li></ul>
<p>Save the request itself to a collection (Ctrl+S or Cmd+S) so teammates in the same workspace get the URL, headers, auth and saved messages too. Connections you open are also recorded in your history, and reopening a history entry brings you back to the WebSocket editor.</p>
<h2 id="a-small-websocket-server-to-test-against">A small WebSocket server to test against</h2>
<p>If you are building the server yourself, here is a minimal Node.js server using the <code>ws</code> package. It answers a <code>ping</code> message, echoes everything else, and broadcasts a timestamp every five seconds so you can see server-initiated messages.</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-js">// server.mjs  (npm install ws)
import { WebSocketServer } from 'ws';

const port = Number(process.env.PORT) || 8080;
const wss = new WebSocketServer({ port });

wss.on('connection', (socket, req) =&gt; {
  const token = new URL(req.url, 'http://x').searchParams.get('token');
  if (token !== process.env.WS_TOKEN) {
    socket.close(1008, 'invalid token'); // 1008 = policy violation
    return;
  }
  socket.send(JSON.stringify({ type: 'welcome' }));

  socket.on('message', (data, isBinary) =&gt; {
    if (!isBinary &amp;&amp; data.toString() === 'ping') return socket.send('pong');
    socket.send(data, { binary: isBinary }); // echo
  });
});

setInterval(() =&gt; {
  const msg = JSON.stringify({ type: 'tick', at: new Date().toISOString() });
  for (const client of wss.clients) if (client.readyState === 1) client.send(msg);
}, 5000);

console.log(`listening on ${port}`);</code></pre></div>
<p>Deploy it behind HTTPS, then connect to <code>wss://your-host/?token={{wsToken}}</code> with <code>wsToken</code> defined in an environment. You should see the <code>welcome</code> message, a <code>tick</code> every five seconds, and your own messages echoed back. Try a wrong token to see how a policy close (code 1008) looks in the log.</p>
<h2 id="things-worth-testing-on-any-websocket-api">Things worth testing on any WebSocket API</h2>
<p>A connection that works once is not the same as a robust API. A few checks catch most real-world bugs:</p>
<ul><li><strong>Auth failures</strong>: connect with no token, an expired token and a token for another tenant. The server should refuse the handshake or close with a clear code, not accept and then ignore you.</li><li><strong>Malformed input</strong>: send invalid JSON, an unknown <code>type</code>, a huge message and an empty message. The server should answer with an error message or close politely, and the connection should not hang.</li><li><strong>Ordering and correlation</strong>: if your protocol uses request ids, send several commands quickly and check that each reply carries the right id.</li><li><strong>Idle behaviour</strong>: leave the connection open. Does the server close idle sockets, and after how long? Use the ping interval setting to keep it alive and compare.</li><li><strong>Reconnection</strong>: turn on automatic reconnection, then restart your server. Check what your server sends to a client that reconnects, and whether subscriptions need to be repeated.</li><li><strong>Binary frames</strong>: if you accept binary data, send a few bytes as hex (for example <code>01 02 ff</code>) and confirm they arrive unchanged.</li></ul>
<h2 id="troubleshooting">Troubleshooting</h2>
<p><strong>&quot;Sign in to use WebSocket, gRPC, MQTT and other protocols in the browser.&quot;</strong> Sending protocol traffic from the web app needs an account, because it goes through the swastik.codes relay. <a href="https://swastik.codes/signup">Create a free account</a> and try again.</p>
<p><strong>The connection is refused immediately for a local or private address.</strong> The relay blocks private and loopback addresses on purpose. Use a public hostname.</p>
<p><strong>Handshake returns 400.</strong> Check the subprotocols in <strong>Settings</strong>. If the server requires a specific subprotocol (some GraphQL and STOMP servers do), it must be listed; if it does not support the one you sent, some servers reject the upgrade.</p>
<p><strong>Messages arrive, but they look like gibberish.</strong> The server is sending binary frames, often MessagePack or Protobuf. Switch the log view to hexdump to inspect the bytes.</p>
<p><strong>The connection closes after about a minute of silence.</strong> Many proxies and load balancers close idle connections. Set a ping interval in <strong>Settings</strong>, and consider sending application-level heartbeats from your server.</p>
<p><strong>Close code 1006.</strong> This code is never sent on the wire; it means the connection dropped without a close frame. Look at your server logs and any proxy in front of it.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="can-i-test-a-websocket-api-without-installing-anything">Can I test a WebSocket API without installing anything?</h3>
<p>Yes. Swastik Codes runs in the browser. You need a free account to send traffic, but there is nothing to install.</p>
<h3 id="can-i-connect-to-ws-localhost-from-the-web-app">Can I connect to ws://localhost from the web app?</h3>
<p>No. The web app relays connections through the swastik.codes server, which refuses private and loopback addresses. Use the <a href="https://swastik.codes/download/">desktop app</a>, deploy the server to a public host, or expose it through a tunnel with a public HTTPS hostname.</p>
<h3 id="does-it-support-binary-websocket-messages">Does it support binary WebSocket messages?</h3>
<p>Yes. Choose <strong>Binary</strong> as the message format and write the payload as Base64 or hexadecimal. Received binary frames can be viewed as a hexdump.</p>
<h3 id="how-do-i-send-an-auth-token-in-a-websocket-handshake">How do I send an auth token in a WebSocket handshake?</h3>
<p>Use the <strong>Authorization</strong> tab (Bearer, Basic, API key or OAuth 2.0), add a header on the <strong>Headers</strong> tab, or put the token in a query parameter on the <strong>Params</strong> tab. Which one works depends on your server; browser-based clients often use a query parameter because browsers cannot set handshake headers.</p>
<h3 id="is-socket-io-the-same-as-websocket">Is Socket.IO the same as WebSocket?</h3>
<p>No. Socket.IO runs its own protocol on top of WebSocket or HTTP long-polling, so a plain WebSocket client cannot talk to a Socket.IO server properly. Use the Socket.IO request type instead; see <a href="https://swastik.codes/blog/test-socket-io-server/">how to test a Socket.IO server</a>.</p>
<h2 id="try-it">Try it</h2>
<p>Open <a href="https://swastik.codes/app">Swastik Codes</a>, create a WebSocket request and connect to your endpoint or to <code>wss://echo.websocket.org</code>. If you do not have an account yet, <a href="https://swastik.codes/signup">sign up for free</a>. The <a href="https://swastik.codes/features/websocket-client/">WebSocket client feature page</a> lists every option, and if your API streams in one direction only, read <a href="https://swastik.codes/blog/test-server-sent-events/">how to test Server-Sent Events</a>. For HTTP endpoints that sit next to your socket server, the <a href="https://swastik.codes/features/rest-api-client/">REST API client</a> works the same way.</p>]]></content:encoded>
</item>
<item>
<title>How to test an MCP server online (tools, resources, prompts)</title>
<link>https://swastik.codes/blog/test-mcp-server/</link>
<guid isPermaLink="true">https://swastik.codes/blog/test-mcp-server/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>MCP</category><category>AI</category><category>API testing</category><category>Tutorial</category>
<description>Connect to a remote MCP server from your browser, list its tools, resources and prompts, call them with arguments and debug MCP OAuth step by step.</description>
<content:encoded><![CDATA[<p>To test an MCP server online, connect an MCP client to the server's HTTP endpoint, let it run the handshake, then list and call the server's tools, resources and prompts while watching the raw JSON-RPC traffic. In Swastik Codes that is an MCP request: paste the URL (for example <code>https://your-server.example.com/mcp</code>), press <strong>Connect</strong>, and the server's tools, resources and prompts appear in tabs. Each tool gets a form generated from its input schema, and every result, notification and error is logged.</p>
<p>This guide covers what the client does at each step, how to call tools with arguments, how to test the parts of MCP that are hard to exercise from a chat client (elicitation, sampling, roots), and how to debug MCP's OAuth flow.</p>
<h2 id="remote-mcp-servers-only-in-the-browser">Remote MCP servers only (in the browser)</h2>
<p>The Model Context Protocol has two kinds of transport:</p>
<ul><li><strong>STDIO</strong> – the client starts the server as a local process and talks to it over standard input and output. This is how most desktop AI tools run local servers.</li><li><strong>HTTP</strong> – the server is a web service. The current transport is <strong>Streamable HTTP</strong> (a single endpoint, usually <code>/mcp</code>, that answers POSTs with JSON or an SSE stream). Older servers use the legacy <strong>HTTP + SSE</strong> transport, with a separate SSE endpoint.</li></ul>
<p>The Swastik Codes web app supports the two HTTP transports. STDIO is not available in the browser: the web app sends traffic through the swastik.codes server, and that server never starts local processes. The transport selector shows the STDIO option disabled for that reason. The same server-side relay also refuses private and loopback addresses, so the MCP server has to be reachable from the internet. If you are building a server on your laptop, deploy it to a staging host or put it behind a tunnel with a public hostname. The <a href="https://swastik.codes/download/">desktop app</a> for Windows and Linux connects from your own machine, so it reaches local servers and can also start STDIO servers.</p>
<p>Sending requests requires a free account, because they go through the server. <a href="https://swastik.codes/signup">Create one</a> if you have not already.</p>
<p>For a public server to practise on, DeepWiki runs a Streamable HTTP MCP server at <code>https://mcp.deepwiki.com/mcp</code> that needs no key for public repositories. We use it below.</p>
<h2 id="step-1-create-an-mcp-request-and-connect">Step 1: create an MCP request and connect</h2>
<ol><li>Open <a href="https://swastik.codes/app">the app</a> and click <strong>+</strong> in the tab bar, then choose <strong>MCP</strong>.</li><li>Enter <code>https://mcp.deepwiki.com/mcp</code> as the URL.</li><li>Leave <strong>Transport</strong> on <strong>Streamable HTTP</strong>. Choose <strong>HTTP + SSE (legacy)</strong> only for servers that still use the older transport.</li><li>Press <strong>Connect</strong>.</li></ol>
<p>With Streamable HTTP and the protocol version on <strong>auto</strong>, the client starts with the classic <code>initialize</code> handshake (with an <code>Mcp-Session-Id</code> header, a GET stream for server messages, and a DELETE when you disconnect), tries the newer stateless handshake (<code>server/discover</code>) only if a server refuses it, and finally falls back to the legacy HTTP + SSE transport. You do not need to know in advance which one a server speaks.</p>
<p>Once connected, a summary line shows:</p>
<ul><li>the server name and version from <code>serverInfo</code>,</li><li>the negotiated protocol version and whether it is a stateless protocol or a session established with <code>initialize</code>,</li><li>the transport, the session id when there is one,</li><li>the capabilities the server declared (for example <code>tools, prompts, resources</code>),</li><li>the server's <strong>Instructions</strong>, collapsed, when it sent any.</li></ul>
<p>For DeepWiki you will see a FastMCP-based server negotiating <code>2025-06-18</code>, with tools, prompts and resources capabilities.</p>
<h2 id="step-2-list-and-call-tools">Step 2: list and call tools</h2>
<p>The <strong>Tools</strong> tab lists every tool from <code>tools/list</code> with its description. Selecting one shows its <strong>Input schema</strong> and, when the server provides one, its <strong>Output schema</strong>. Arguments are entered in a form generated from the JSON schema, with a JSON view when you prefer to type the object yourself.</p>
<p>Select <code>read_wiki_structure</code>, enter a repository in <code>owner/repo</code> form for <code>repoName</code>, and press <strong>Run tool</strong>. The result panel shows the <code>content</code> blocks the tool returned, and <code>structuredContent</code> when the tool has an output schema. A tool that fails with <code>isError: true</code> is shown as an error, which is different from a JSON-RPC error (an unknown tool, or invalid parameters). Testing both cases is worthwhile:</p>
<ul><li>call the tool with a valid repository and check the result shape,</li><li>call it with a missing required argument and check that the server answers with a useful error instead of crashing,</li><li>call it with a repository that does not exist and check the error is reported in the tool result.</li></ul>
<p>A search box (<strong>Search tools, resources and prompts</strong>) filters long lists, and <strong>Refresh</strong> reloads them, which is useful when a server sends <code>notifications/tools/list_changed</code>.</p>
<h2 id="step-3-resources-and-prompts">Step 3: resources and prompts</h2>
<p>The <strong>Resources</strong> tab lists <code>resources/list</code> and resource templates from <code>resources/templates/list</code>. Enter or pick a URI and press <strong>Read resource</strong> to see the contents. The <strong>Prompts</strong> tab lists <code>prompts/list</code>; fill in the prompt's arguments and press <strong>Get prompt</strong> to see the messages the server would hand to a model.</p>
<p>If a server does not declare a capability, the corresponding list is simply empty rather than an error.</p>
<h2 id="step-4-watch-notifications">Step 4: watch notifications</h2>
<p>The <strong>Notifications</strong> tab collects everything the server sends on its own: progress notifications, log messages, list-changed notifications and server-to-client requests. In the <strong>Configuration</strong> tab you can set the <strong>Server log level</strong> (sent as <code>logging/setLevel</code> on session-based connections, or in the request metadata on the stateless protocol), so a server that supports logging streams its debug output into this tab while you call tools.</p>
<p>The Configuration tab is also where you set:</p>
<ul><li><strong>Protocol version</strong> – <code>auto</code>, or a specific revision such as <code>2026-07-28</code>, <code>2025-11-25</code>, <code>2025-06-18</code>, <code>2025-03-26</code> or <code>2024-11-05</code>. Pinning a version is the quickest way to check backwards compatibility.</li><li><strong>Request timeout (ms)</strong> – for slow tools.</li><li><strong>Verify TLS certificates</strong> – leave it on unless you are testing a staging host with a self-signed certificate.</li><li><strong>Headers</strong> – extra HTTP headers sent with every MCP request, for example an organisation header or a static API key.</li></ul>
<h2 id="step-5-elicitation-sampling-and-roots">Step 5: elicitation, sampling and roots</h2>
<p>These features are requests from the <em>server</em> to the <em>client</em>. They are hard to test from a chat application because the application decides how to answer. In Swastik Codes you answer them yourself:</p>
<ul><li><strong>Elicitation (ask the user for input).</strong> Turn it on in Configuration and the client advertises the capability. When the server sends <code>elicitation/create</code>, the request is held and shown as <em>Input required</em>. You fill in the requested fields and answer, or decline. This lets you test both the happy path and what your tool does when the user refuses.</li><li><strong>Sampling (the user writes the model's reply).</strong> With sampling advertised, a <code>sampling/createMessage</code> request is shown with the messages the server wants completed. You type the reply as the assistant, set the model name reported back, and send it. You can also reject the request. This is a deterministic way to test tools that call back into a model.</li><li><strong>Roots.</strong> List <code>file://</code> URIs (with optional names) under <strong>Roots</strong>; they are returned to the server's <code>roots/list</code> request. Changing them sends <code>notifications/roots/list_changed</code> on session-based connections.</li></ul>
<p>Held server requests wait until you answer, for up to 10 minutes.</p>
<h2 id="step-6-authorization-and-the-mcp-oauth-debugger">Step 6: authorization and the MCP OAuth debugger</h2>
<p>Remote MCP servers that need a user's consent use OAuth 2.1, discovered from the server itself. When a server answers <code>401</code>, the <strong>Authorization</strong> tab is marked. It contains the normal auth editor (Bearer token, API key, OAuth 2.0 and so on) and a step-by-step OAuth debugger. Press <strong>Run discovery</strong> and it performs, and reports, each step the MCP authorization specification describes:</p>
<ol><li><strong>Probe the server</strong> – send <code>initialize</code> and read the <code>WWW-Authenticate</code> header on the 401.</li><li><strong>Protected resource metadata (RFC 9728)</strong> – find which authorization server protects this MCP server.</li><li><strong>Authorization server metadata (RFC 8414)</strong> – read its endpoints, with a warning when <code>code_challenge_methods_supported</code> does not include <code>S256</code>, which MCP clients require.</li><li><strong>Client registration (RFC 7591)</strong> – register a client dynamically, or skip this when you enter an <strong>OAuth client ID</strong> (and secret) yourself.</li></ol>
<p>Each step shows the request outcome and the metadata it found, which makes it clear exactly where a misconfigured server breaks. When discovery succeeds, the request's OAuth 2.0 settings are filled in, and <strong>Authorize</strong> runs the authorization code flow with PKCE in a popup. The web app's callback URL is <code>https://swastik.codes/oauth/callback</code>; register it with your authorization server if it does not use dynamic registration. The token is then sent as a Bearer token on every MCP request. The general OAuth flow is explained in <a href="https://swastik.codes/blog/oauth2-in-api-client/">OAuth 2.0 in an API client</a>.</p>
<h2 id="a-minimal-server-to-test-against">A minimal server to test against</h2>
<p>If you want a server of your own, the official Python SDK can serve Streamable HTTP in a few lines:</p>
<div class="code"><div class="code-head"><span>Python</span></div><pre tabindex="0"><code class="language-python"># server.py  –  pip install &quot;mcp[cli]&quot;
from mcp.server.fastmcp import FastMCP

mcp = FastMCP(&quot;demo&quot;)

@mcp.tool()
def add(a: int, b: int) -&gt; int:
    &quot;&quot;&quot;Add two numbers.&quot;&quot;&quot;
    return a + b

@mcp.resource(&quot;greeting://{name}&quot;)
def greeting(name: str) -&gt; str:
    &quot;&quot;&quot;A personalised greeting.&quot;&quot;&quot;
    return f&quot;Hello, {name}!&quot;

if __name__ == &quot;__main__&quot;:
    mcp.run(transport=&quot;streamable-http&quot;)  # serves http://0.0.0.0:8000/mcp</code></pre></div>
<p>Deploy it somewhere public (or expose it through a tunnel), connect to <code>https://&lt;your-host&gt;/mcp</code>, and you should see one tool (<code>add</code>) and one resource template (<code>greeting://{name}</code>). Call <code>add</code> with <code>{&quot;a&quot;: 2, &quot;b&quot;: 3}</code> and the result is <code>5</code>. Check the SDK documentation for the version you install, as the APIs evolve.</p>
<h2 id="sharing-and-reusing-what-you-tested">Sharing and reusing what you tested</h2>
<p>An MCP request can be saved into a collection like any other request, so the URL, headers, auth and configuration are kept with your other API work and shared with your team when the collection lives in a team workspace. Connections are also recorded in your history. When you are happy with a server, <strong>Export configuration</strong> produces the JSON snippet to add it to Claude Desktop, Claude Code, VS Code or Cursor.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<p><strong>Connection fails immediately.</strong> Check the path (<code>/mcp</code> is common, but not universal) and that the host is public. Private and loopback addresses are refused by the swastik.codes relay.</p>
<p><strong>401 Unauthorized.</strong> Open the Authorization tab and run discovery. If step 2 fails, the server does not publish protected resource metadata; enter the authorization server details manually in the OAuth 2.0 settings.</p>
<p><strong>Tools list is empty.</strong> The server did not declare the <code>tools</code> capability, or it requires auth before listing. Look at the capabilities in the summary line.</p>
<p><strong>Works with one client, not another.</strong> Pin the protocol version in Configuration and reconnect. Differences between <code>2024-11-05</code> and later revisions (for example, JSON-RPC batching and structured tool output) are a common cause.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="is-this-an-alternative-to-the-mcp-inspector">Is this an alternative to the MCP Inspector?</h3>
<p>For remote servers, it covers the same ground (listing and calling tools, resources and prompts, viewing notifications) and adds saved requests, team workspaces, history and an OAuth discovery debugger. The official MCP Inspector runs on your machine and can start STDIO servers, which the Swastik Codes web app cannot. Many developers use both.</p>
<h3 id="can-i-test-a-local-stdio-mcp-server">Can I test a local STDIO MCP server?</h3>
<p>Not from the web app. It supports Streamable HTTP and the legacy HTTP + SSE transport, and the server must be publicly reachable. The <a href="https://swastik.codes/download/">desktop app</a> for Windows and Linux can start a local STDIO server.</p>
<h3 id="which-mcp-protocol-versions-are-supported">Which MCP protocol versions are supported?</h3>
<p>Auto-negotiation plus explicit versions from <code>2024-11-05</code> to <code>2026-07-28</code>, including the stateless handshake of the newest revision.</p>
<h3 id="do-i-need-an-account">Do I need an account?</h3>
<p>The web app needs one, because connections go through the swastik.codes relay.</p>
<h2 id="try-it">Try it</h2>
<p><a href="https://swastik.codes/app">Open the app</a>, add an MCP request for <code>https://mcp.deepwiki.com/mcp</code>, and press Connect. Sending requires a <a href="https://swastik.codes/signup">free account</a>. The <a href="https://swastik.codes/features/mcp-client/">MCP client feature page</a> lists everything the client supports. If your server also exposes a plain REST or SSE interface, the <a href="https://swastik.codes/features/rest-api-client/">REST client</a> and <a href="https://swastik.codes/features/sse-client/">SSE client</a> live in the same workspace.</p>]]></content:encoded>
</item>
<item>
<title>How to test Server-Sent Events (SSE) endpoints online</title>
<link>https://swastik.codes/blog/test-server-sent-events/</link>
<guid isPermaLink="true">https://swastik.codes/blog/test-server-sent-events/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>SSE</category><category>Streaming</category><category>Testing</category>
<description>Test an SSE endpoint from your browser: named events, ids, Last-Event-ID resume, retry and POST streams such as LLM APIs, with custom headers and auth.</description>
<content:encoded><![CDATA[<p>To test a Server-Sent Events endpoint you need a client that sends the request, keeps the response open, parses the <code>text/event-stream</code> format as it arrives and shows each event with its type, id and data. In Swastik Codes you create a <strong>Server-Sent Events</strong> request, paste the URL, add any headers or auth, choose the HTTP method (GET, or POST for streaming APIs that take a body), and press <strong>Connect</strong>. Events appear in the log as they arrive.</p>
<p>The browser's own <code>EventSource</code> is a poor testing tool: it only does GET, it cannot set custom headers such as <code>Authorization</code>, and it hides the raw stream. This guide covers what a proper SSE client shows you, how to test resumption and reconnection, and how to test POST-based streams like the ones large language model APIs use.</p>
<h2 id="what-you-need">What you need</h2>
<ul><li>An SSE endpoint reachable from the public internet. The Swastik Codes web app relays the connection through the swastik.codes server, which refuses private, loopback and other reserved addresses, so <code>http://localhost:3000/events</code> cannot be reached from the web app. The <a href="https://swastik.codes/download/">desktop app</a> for Windows and Linux connects from your own machine, so it reaches local endpoints.</li><li>A free Swastik Codes account, because streaming connections run through the server.</li></ul>
<p>If you want to try the client first, Wikimedia's public EventStreams service is a real SSE endpoint: <code>https://stream.wikimedia.org/v2/stream/recentchange</code> streams every edit on Wikimedia projects as it happens. It is a busy stream, so expect many events per second, and disconnect when you are done.</p>
<h2 id="how-the-sse-format-works">How the SSE format works</h2>
<p>An SSE response has the content type <code>text/event-stream</code> and is a sequence of events separated by blank lines. Each event is made of fields:</p>
<div class="code"><div class="code-head"><span>Text</span></div><pre tabindex="0"><code class="language-text">event: order.updated
id: 1042
data: {&quot;orderId&quot;: &quot;A-17&quot;, &quot;status&quot;: &quot;shipped&quot;}

: this line is a comment, often used as a heartbeat

retry: 5000
data: a message without an event type is a &quot;message&quot; event
data: and multiple data lines are joined with newlines
</code></pre></div>
<ul><li><code>data:</code> is the payload. Several <code>data:</code> lines in one event are joined with <code>\n</code>.</li><li><code>event:</code> is the event type. Without it, the type is <code>message</code>.</li><li><code>id:</code> sets the last event id. A client that reconnects sends it back in the <code>Last-Event-ID</code> request header so the server can resume.</li><li><code>retry:</code> tells the client how many milliseconds to wait before reconnecting.</li><li>Lines starting with <code>:</code> are comments and are ignored.</li></ul>
<p>Swastik Codes parses the stream incrementally following the WHATWG event stream rules, including events that arrive split across network packets and UTF-8 characters split in the middle.</p>
<h2 id="step-1-create-an-sse-request">Step 1: create an SSE request</h2>
<ol><li>Open <a href="https://swastik.codes/app">the app</a> and sign in.</li><li>Click <strong>+</strong> in the tab bar and choose <strong>Server-Sent Events</strong> from the <strong>New request</strong> menu.</li><li>Paste the URL, for example <code>https://stream.wikimedia.org/v2/stream/recentchange</code>.</li><li>Leave the method on <strong>GET</strong> and press <strong>Connect</strong>.</li></ol>
<p>The client sends <code>Accept: text/event-stream</code> and <code>Cache-Control: no-cache</code> for you. If the server answers with a different content type, the log shows a note and the body is parsed as an event stream anyway, which helps when a misconfigured proxy rewrites the header.</p>
<p>Each event in the log shows its type, data, id and arrival time. Use <strong>View as</strong> to switch between automatic detection, text and JSON, search the log, and select two events to see the time between them. <strong>Connection details</strong> shows the request and response headers, which is where you check for proxy problems such as a missing <code>Content-Type</code> or an unexpected <code>Content-Encoding</code>.</p>
<h2 id="step-2-headers-auth-and-parameters">Step 2: headers, auth and parameters</h2>
<p>Most private SSE endpoints authenticate with a header. The <strong>Authorization</strong> tab supports Bearer, Basic, API key and OAuth 2.0, or inheriting auth from the collection. <strong>Headers</strong> takes any other header, and <strong>Params</strong> edits the query string. Use variables like <code>{{baseUrl}}</code> and <code>{{token}}</code> so the same request works across environments.</p>
<h2 id="step-3-filter-event-types">Step 3: filter event types</h2>
<p>Busy streams mix many event types. Type the types you care about into the event filter, comma separated (for example <code>order.updated, order.cancelled</code>), and only those are shown. The filter only affects the display; the connection still receives everything.</p>
<h2 id="step-4-test-resumption-with-last-event-id">Step 4: test resumption with Last-Event-ID</h2>
<p>Resumption is the feature that makes SSE more than a long HTTP response, and it is the part most often broken in server implementations. To test it:</p>
<ol><li>Connect and let a few events with ids arrive.</li><li>The id of the most recent event is shown above the log with a <strong>Use</strong> button. Click it to copy that id into the <strong>Last-Event-ID</strong> setting.</li><li>Disconnect, wait a moment, and connect again.</li></ol>
<p>The client now sends <code>Last-Event-ID: &lt;id&gt;</code> with the request. A correct server sends the events after that id and nothing before it. Check for gaps and duplicates at the boundary. You can also type any id into <strong>Last-Event-ID</strong> by hand, for example an old id to test what the server does when the id is too old to replay.</p>
<h2 id="step-5-test-reconnection-and-retry">Step 5: test reconnection and retry</h2>
<p>In <strong>Settings</strong>, turn on <strong>Reconnect when the stream ends (honours retry:)</strong>. When the server closes the stream, the client waits and reconnects with the last id it saw, just like <code>EventSource</code> does. The wait starts at the <strong>Reconnect delay (ms)</strong> and switches to the server's <code>retry:</code> value once one arrives. <strong>Reconnection attempts</strong> limits the number of tries (0 means unlimited). The log shows each reconnect with the delay and the <code>Last-Event-ID</code> that was sent.</p>
<p>Other settings: <strong>Connection timeout (ms)</strong> for the initial response, <strong>Maximum event size (MB)</strong> (larger events are discarded and reported, so a stream that never sends a newline cannot grow without bound), and server certificate verification.</p>
<h2 id="step-6-post-streams-and-llm-style-apis">Step 6: POST streams and LLM-style APIs</h2>
<p>Many APIs stream with SSE in response to a POST request with a JSON body. Chat completion endpoints of large language model providers are the best-known example: you POST a prompt with <code>&quot;stream&quot;: true</code> and receive tokens as <code>data:</code> events, often ending with a sentinel such as <code>data: [DONE]</code>. <code>EventSource</code> cannot do this at all.</p>
<p>In Swastik Codes, pick <strong>POST</strong> (or PUT, PATCH, DELETE) in the method dropdown next to the URL, then open the <strong>Body</strong> tab and write the JSON body:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{
  &quot;model&quot;: &quot;{{model}}&quot;,
  &quot;stream&quot;: true,
  &quot;messages&quot;: [{ &quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Explain Server-Sent Events in one sentence.&quot; }]
}</code></pre></div>
<p>Add the provider's auth header (usually a bearer token or an API key header) and a <code>Content-Type: application/json</code> header, then press <strong>Connect</strong>. Each chunk arrives as an event, so you can see exactly how the stream is split, how long the first token takes, and how the stream ends. Keep API keys in an environment variable rather than in the request, especially in shared workspaces.</p>
<h2 id="a-small-sse-server-to-test-against">A small SSE server to test against</h2>
<p>This Node.js server uses only the standard library. <code>GET /events</code> sends numbered events with ids, honours <code>Last-Event-ID</code> and sends a heartbeat comment. <code>POST /stream</code> streams back the words of the <code>text</code> field from the JSON body, then a <code>done</code> event.</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-js">// server.mjs  (Node.js 18+, no dependencies)
import { createServer } from 'node:http';

const headers = {
  'Content-Type': 'text/event-stream',
  'Cache-Control': 'no-cache',
  'X-Accel-Buffering': 'no', // stop nginx from buffering the stream
};

createServer(async (req, res) =&gt; {
  if (req.method === 'GET' &amp;&amp; req.url.startsWith('/events')) {
    res.writeHead(200, headers);
    let id = Number(req.headers['last-event-id']) || 0;
    res.write('retry: 3000\n\n');
    const tick = setInterval(() =&gt; {
      id += 1;
      const type = id % 5 === 0 ? 'order.cancelled' : 'order.updated';
      res.write(`event: ${type}\nid: ${id}\ndata: ${JSON.stringify({ seq: id, at: new Date().toISOString() })}\n\n`);
    }, 1000);
    const heartbeat = setInterval(() =&gt; res.write(': ping\n\n'), 15000);
    req.on('close', () =&gt; { clearInterval(tick); clearInterval(heartbeat); });
    return;
  }

  if (req.method === 'POST' &amp;&amp; req.url === '/stream') {
    let raw = '';
    for await (const chunk of req) raw += chunk;
    const { text = 'hello from the stream' } = JSON.parse(raw || '{}');
    res.writeHead(200, headers);
    for (const word of String(text).split(/\s+/)) {
      res.write(`data: ${JSON.stringify({ token: word })}\n\n`);
      await new Promise((r) =&gt; setTimeout(r, 200));
    }
    res.write('event: done\ndata: [DONE]\n\n');
    return res.end();
  }

  res.writeHead(404).end();
}).listen(Number(process.env.PORT) || 3000);</code></pre></div>
<p>Deploy it and try:</p>
<ol><li><code>GET https://your-host/events</code>: events arrive every second, every fifth one an <code>order.cancelled</code>. Filter on <code>order.cancelled</code>.</li><li>Click <strong>Use</strong> next to the last id, disconnect, wait five seconds and reconnect. The sequence continues from that id.</li><li>Turn on reconnection and restart the server: the client waits <code>retry</code> milliseconds and resumes.</li><li><code>POST https://your-host/stream</code> with body <code>{&quot;text&quot;: &quot;one two three four&quot;}</code>: four <code>data</code> events and a <code>done</code> event.</li></ol>
<h2 id="troubleshooting">Troubleshooting</h2>
<p><strong>Nothing arrives for a long time, then everything at once.</strong> A proxy is buffering the response. For nginx, set <code>proxy_buffering off;</code> for the location or send <code>X-Accel-Buffering: no</code> from the app. Compression middleware can also buffer: exclude <code>text/event-stream</code> from it.</p>
<p><strong>The connection drops after 30 or 60 seconds.</strong> An idle timeout on a load balancer or proxy. Send a comment line (<code>: ping</code>) every 15 to 30 seconds.</p>
<p><strong>Events show up with the type <code>message</code> when you expected a name.</strong> The server is not sending an <code>event:</code> field, or it is sending it after <code>data:</code> in a different event block. Each event ends at a blank line.</p>
<p><strong>JSON data is cut in half.</strong> The server wrote a raw newline inside the data. Each line of the payload needs its own <code>data:</code> prefix, or serialise the JSON on one line.</p>
<p><strong>Connection refused for a private address.</strong> The relay blocks private and loopback addresses on purpose. Use a public hostname.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="what-is-the-difference-between-sse-and-websocket">What is the difference between SSE and WebSocket?</h3>
<p>SSE is one-way, server to client, over plain HTTP, with built-in resumption through event ids. WebSocket is a two-way channel with its own framing. If clients only need to receive updates, SSE is simpler to run behind ordinary HTTP infrastructure. See <a href="https://swastik.codes/blog/test-websocket-api-online/">how to test a WebSocket API</a> for the other side.</p>
<h3 id="can-i-send-custom-headers-to-an-sse-endpoint">Can I send custom headers to an SSE endpoint?</h3>
<p>Yes. Unlike the browser's <code>EventSource</code>, Swastik Codes can send any headers, including <code>Authorization</code>, and supports Bearer, Basic, API key and OAuth 2.0 auth.</p>
<h3 id="can-i-test-sse-streams-that-use-post">Can I test SSE streams that use POST?</h3>
<p>Yes. Choose POST, PUT, PATCH or DELETE next to the URL and write the request body on the <strong>Body</strong> tab. This covers streaming LLM APIs and other POST-based streams.</p>
<h3 id="how-do-i-test-last-event-id-resumption">How do I test Last-Event-ID resumption?</h3>
<p>Connect, click <strong>Use</strong> next to the last id shown above the log, disconnect and reconnect. The client sends that id in the <code>Last-Event-ID</code> header. You can also type an id into the setting by hand.</p>
<h3 id="does-mcp-use-sse">Does MCP use SSE?</h3>
<p>Some MCP servers use SSE as a transport, either the older HTTP+SSE transport or streamed responses in the Streamable HTTP transport. For those, use the dedicated MCP request type described in <a href="https://swastik.codes/blog/test-mcp-server/">how to test an MCP server</a>, which handles the MCP handshake for you.</p>
<h2 id="try-it">Try it</h2>
<p>Open <a href="https://swastik.codes/app">Swastik Codes</a>, create a Server-Sent Events request and connect to your endpoint, or to Wikimedia's public stream to see one in action. You need a <a href="https://swastik.codes/signup">free account</a> to connect. The <a href="https://swastik.codes/features/sse-client/">SSE client feature page</a> lists every option, and the <a href="https://swastik.codes/features/rest-api-client/">REST API client</a> covers the non-streaming endpoints of the same API.</p>]]></content:encoded>
</item>
<item>
<title>Moving from Postman to a web-based API client: what carries over and what to check</title>
<link>https://swastik.codes/blog/migrate-from-postman/</link>
<guid isPermaLink="true">https://swastik.codes/blog/migrate-from-postman/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>Postman</category><category>Migration</category><category>Collections</category>
<description>A practical guide to moving collections, environments, pm.* scripts and teams from Postman to Swastik Codes, and what to check once you have.</description>
<content:encoded><![CDATA[<p>If you are looking for a Postman alternative that runs in the browser, the good news is that most of your work can move with you. Swastik Codes reads the public Postman Collection format (v2.0 and v2.1, plus v1 and the newer v3), Postman environment and globals files, and full Postman data dumps. Your <code>pm.*</code> pre-request and test scripts run in a sandbox that implements the documented <code>pm</code> API, so in most collections you import the files, pick an environment and press Send.</p>
<p>This guide covers what carries over, what to check after the move, how to set up a team, and where the two tools differ. It is written for people who already know Postman well and want a realistic picture before they switch, or before they run both side by side.</p>
<h2 id="the-short-version">The short version</h2>
<ol><li>Export your collections, environments and globals from Postman as JSON (or export a full data dump).</li><li>Open <a href="https://swastik.codes/app">Swastik Codes</a>, click <strong>Import</strong> and drop the files in. The format is detected for you.</li><li>Create a free account so the web app can send requests, then pick an environment and run a request.</li><li>Run the collection in the <a href="https://swastik.codes/features/collection-runner/">collection runner</a> and compare the test results with what you see in Postman.</li><li>Invite your team to a team workspace with admin, editor or viewer roles.</li></ol>
<p>The step-by-step import itself, including screenshots of each option, is in <a href="https://swastik.codes/blog/import-postman-collection/">How to import a Postman collection</a>. The rest of this article is about everything around it.</p>
<h2 id="what-carries-over">What carries over</h2>
<p>Swastik Codes was built against public, documented formats, so a collection keeps its structure rather than being flattened or approximated.</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Postman item</th><th>In Swastik Codes</th></tr></thead><tbody><tr><td>Collections (v2.0, v2.1, v1, v3)</td><td>Imported with folders, requests, descriptions and saved examples</td></tr><tr><td>Environments</td><td>Imported as environments; you can pin them to a collection</td></tr><tr><td>Globals</td><td>Imported as workspace globals</td></tr><tr><td>Data dump (everything export)</td><td>Collections, environments and globals are imported together</td></tr><tr><td>Collection and folder variables</td><td>Kept as collection variables</td></tr><tr><td>Pre-request and test scripts</td><td>Kept at collection, folder and request level and run in the same order</td></tr><tr><td>Auth settings</td><td>Kept, including inherit-from-parent</td></tr><tr><td>Saved examples</td><td>Kept, and usable by <a href="https://swastik.codes/features/mock-server/">mock servers</a></td></tr><tr><td>Request settings</td><td>Redirects, SSL verification, timeouts and similar per-request settings are mapped</td></tr></tbody></table></div>
<p>A few things in a data dump are not imported: header presets, mocks and monitors. When the dump contains any of these, the import preview lists a note such as &quot;<code>mocks</code> in the data dump are not imported&quot;. You recreate mocks and monitors in Swastik Codes from the imported collection, which usually takes a minute each.</p>
<h3 id="scripts-and-tests">Scripts and tests</h3>
<p>Scripts run in an isolated JavaScript sandbox with time and memory limits. Swastik Codes calls its script API <code>swc</code>; Postman's <code>pm</code> is the same object, so scripts written for Postman run as they are. The API covers the parts real collections use:</p>
<ul><li>variables: <code>swc.variables</code>, <code>swc.environment</code>, <code>swc.globals</code>, <code>swc.collectionVariables</code>, <code>swc.iterationData</code>, including <code>replaceIn</code>;</li><li><code>swc.request</code>, <code>swc.response</code>, <code>swc.cookies</code> and <code>swc.info</code>;</li><li><code>swc.test</code> (and <code>swc.test.skip</code>) with <code>swc.expect</code> and the chai-style <code>swc.response.to.have.status(...)</code>, <code>.header(...)</code>, <code>.jsonBody(...)</code> and <code>.jsonSchema(...)</code> assertions;</li><li><code>swc.sendRequest</code> with a callback or as a promise;</li><li><code>swc.execution.setNextRequest</code> and <code>swc.execution.skipRequest</code> for flow control in runs;</li><li><code>swc.visualizer.set</code> for custom response views.</li></ul>
<p>Commonly required libraries are available through <code>require</code>, including <code>chai</code>, <code>lodash</code>, <code>moment</code>, <code>crypto-js</code>, <code>uuid</code>, <code>ajv</code>, <code>tv4</code>, <code>xml2js</code>, <code>cheerio</code> and <code>csv-parse/lib/sync</code>, plus Node-style modules such as <code>buffer</code>, <code>url</code>, <code>querystring</code> and <code>util</code>. Older scripts that use the legacy <code>postman.setEnvironmentVariable</code>, <code>tests[&quot;...&quot;] = ...</code> and <code>responseBody</code> globals also run.</p>
<p>A typical Postman test script needs no changes:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">pm.test(&quot;status is 200&quot;, () =&gt; {
  pm.response.to.have.status(200);
});

pm.test(&quot;returns a user with an id&quot;, () =&gt; {
  const body = pm.response.json();
  pm.expect(body).to.have.property(&quot;id&quot;);
  pm.environment.set(&quot;userId&quot;, body.id);
});</code></pre></div>
<h3 id="pm-or-swc">pm.* or swc.*</h3>
<p>Both names work, so converting is optional. To keep one name across a workspace:</p>
<ul><li><strong>When importing</strong>, leave <strong>Convert Postman scripts to swc.*</strong> ticked (it is shown when the selected collections contain <code>pm.*</code> scripts). Every collection, folder and request script is rewritten from <code>pm.</code> to <code>swc.</code>.</li><li><strong>Later</strong>, use <strong>Convert pm.* scripts to swc.*</strong> in a collection's menu, or <strong>Convert to swc.*</strong> in the bar shown above a script that uses <code>pm.*</code>.</li><li><strong>When exporting to Postman</strong> (v2.1 or v3), scripts are written back with <code>pm.</code>, so the collection keeps running in Postman.</li></ul>
<p>The converter reads the JavaScript instead of searching for text: strings, regular expressions, <code>npm</code>, <code>obj.pm</code> and object keys named <code>pm</code> are left alone, and a script that already declares its own <code>swc</code> variable is not touched.</p>
<p>If you want to write new tests, <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">Writing API tests with swc.* scripts</a> walks through the patterns in detail.</p>
<h3 id="authorization">Authorization</h3>
<p>All the usual auth types are supported: API key, Bearer token, Basic, Digest, JWT, OAuth 1.0, OAuth 2.0 (authorization code with or without PKCE, implicit, password, client credentials and refresh), Hawk, AWS Signature v4, NTLM, Akamai EdgeGrid and ASAP. Auth set on a collection or folder is inherited the same way. For OAuth 2.0 flows in the browser, see <a href="https://swastik.codes/blog/oauth2-in-api-client/">OAuth 2.0 in an API client</a>.</p>
<h2 id="what-to-check-after-you-move">What to check after you move</h2>
<p>Imports are faithful, but every team has a few habits that are worth a quick look.</p>
<h3 id="1-variable-values-that-were-only-current">1. Variable values that were only &quot;current&quot;</h3>
<p>Postman environments distinguish between initial and current values, and an export may or may not include the current ones depending on how it was made. In Swastik Codes, current values of variables are local to your device: they are stripped before anything is stored on the server. That is good for secrets, but it means a token you only ever kept as a current value will not appear for your teammates. Set the initial value for anything the team needs, and keep personal secrets as current values or in the <a href="https://swastik.codes/features/environments-variables/">vault</a>.</p>
<h3 id="2-secrets-that-should-not-be-shared">2. Secrets that should not be shared</h3>
<p>Before you put a collection in a team or public workspace, run the secret scanner. It looks for API keys, tokens, passwords and private keys stored as plain text in collections, environments and globals, and it tells you which findings would be exposed through a public workspace or published documentation.</p>
<h3 id="3-requests-to-localhost-or-a-private-network">3. Requests to localhost or a private network</h3>
<p>This is the most important difference for a web-based client. A browser cannot send arbitrary cross-origin requests, so the Swastik Codes web app sends your requests through the swastik.codes server. That server refuses private, loopback, link-local and reserved addresses. Requests to <code>http://localhost:3000</code>, <code>192.168.x.x</code> or an internal hostname that resolves to a private IP will fail with an error, even if they work in a desktop client.</p>
<p>For local development, point the request at a publicly reachable staging or preview deployment instead. Or install the <a href="https://swastik.codes/download/">desktop app</a> for Windows or Linux, which sends requests from your own machine and reaches <code>localhost</code> and private networks.</p>
<h3 id="4-scripts-that-depend-on-unusual-libraries-or-timing">4. Scripts that depend on unusual libraries or timing</h3>
<p>Scripts that use the common libraries listed above work as written. If a script loads code from a URL with <code>eval</code>, depends on very long <code>setTimeout</code> chains, or relies on a module that is not in the list, check it after the import. The console panel shows script errors with the line that failed.</p>
<h3 id="5-runner-settings">5. Runner settings</h3>
<p>Iterations, delays, data files and &quot;persist variables&quot; exist in the <a href="https://swastik.codes/features/collection-runner/">collection runner</a>, and <code>setNextRequest</code> works the way it does in Newman. Re-run your main collection once and compare the pass and fail counts with your last Postman run. Differences usually point at a variable that was never set, which is easy to fix.</p>
<h2 id="setting-up-your-team">Setting up your team</h2>
<p>Signing up creates a personal cloud workspace called &quot;My Workspace&quot; that syncs across your devices. For a team:</p>
<ol><li>Open the teams dialog and create a team. You become its admin.</li><li>Invite people by email. Each invite has a role: <strong>admin</strong> manages the team and its workspaces, <strong>editor</strong> changes content, <strong>viewer</strong> reads and runs. Invite links expire after seven days.</li><li>Create a team workspace and import the shared collections there.</li></ol>
<p>Team workspaces get the collaboration features: comments on requests and collections with <code>@</code> mentions, version history with restore, forks and pull requests with reviews and merge checks, and an activity feed. Changes from teammates appear live. The <a href="https://swastik.codes/features/collaboration/">collaboration page</a> covers these in more depth.</p>
<p>You can also keep a workspace on your device only. Local workspaces are stored in the browser, are not synced, and are handy for experiments.</p>
<h2 id="what-is-different">What is different</h2>
<p>It would not be fair to suggest the two tools are identical, so here is an honest list.</p>
<ul><li><strong>Where requests come from.</strong> In the web app, requests are sent by the swastik.codes server rather than your machine. That removes CORS limits, but it means private and loopback targets are refused, and the target server sees our IP address rather than yours. Postman's desktop app sends from your machine.</li><li><strong>Desktop apps.</strong> Swastik Codes runs in the browser, and as a <a href="https://swastik.codes/download/">desktop app</a> for Windows and Linux that sends requests from your machine. A macOS version is coming.</li><li><strong>Ecosystem.</strong> Postman has a large public network of shared collections, integrations and a long history. Swastik Codes focuses on the client, testing and team workflow.</li><li><strong>Pricing.</strong> Swastik Codes is free to use today, with no paid plans. Check Postman's own site for its current plans.</li><li><strong>Protocols.</strong> Besides HTTP, Swastik Codes includes GraphQL, WebSocket, Socket.IO, Server-Sent Events, gRPC, MQTT and MCP clients in the same app.</li></ul>
<p>If you depend on a Postman-specific integration or on reaching private hosts directly, keep Postman for those collections and try Swastik Codes for the rest. Nothing stops you from running both: you can export back to Postman Collection v2.1 at any time. A detailed side-by-side is on the <a href="https://swastik.codes/compare/postman/">Swastik Codes vs Postman</a> page.</p>
<h2 id="a-suggested-migration-plan-for-a-team">A suggested migration plan for a team</h2>
<ol><li><strong>Pick one collection</strong> that the team uses every day and has tests.</li><li><strong>Import it</strong> into a team workspace together with its environments.</li><li><strong>Run it</strong> in the collection runner and fix any differences (usually variables).</li><li><strong>Recreate mocks and monitors</strong> for that collection if you used them.</li><li><strong>Use it for a week.</strong> Leave the Postman copy untouched as a fallback.</li><li><strong>Move the rest</strong> once the team is comfortable, using a data dump if you have many collections.</li></ol>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="will-my-postman-tests-run-without-changes">Will my Postman tests run without changes?</h3>
<p>In most cases, yes. The sandbox implements the documented <code>pm.*</code> API, chai-style assertions and the commonly used libraries. Tests that call private network hosts, or depend on modules outside the supported list, need adjusting.</p>
<h3 id="can-i-import-everything-at-once">Can I import everything at once?</h3>
<p>Yes. Export a data dump from Postman and import it. Collections, environments and globals come across in one go. Header presets, mocks and monitors in the dump are not imported and are listed as notes.</p>
<h3 id="why-cant-i-reach-my-localhost-api">Why can't I reach my localhost API?</h3>
<p>The web app sends requests through the swastik.codes server, which refuses private and loopback addresses for security. Use a publicly reachable staging URL, or the <a href="https://swastik.codes/download/">desktop app</a> for Windows and Linux, which sends from your machine.</p>
<h3 id="can-i-go-back-to-postman-later">Can I go back to Postman later?</h3>
<p>Yes. Export any collection as Postman Collection v2.1 (or v2.0, or v3) and environments in Postman format, then import them into Postman.</p>
<h3 id="is-swastik-codes-free">Is Swastik Codes free?</h3>
<p>Yes, it is free to use today and there are no paid plans.</p>
<h2 id="try-it-with-your-own-collection">Try it with your own collection</h2>
<p>The fastest way to judge any Postman alternative is with a real collection. <a href="https://swastik.codes/signup">Create a free account</a>, open the app and import one. The <a href="https://swastik.codes/features/import-export/">import and export feature page</a> lists every supported format, and the <a href="https://swastik.codes/features/collection-runner/">collection runner</a> is where you check that your tests still pass.</p>]]></content:encoded>
</item>
<item>
<title>MQTT client online: test an MQTT broker in your browser</title>
<link>https://swastik.codes/blog/mqtt-client-online/</link>
<guid isPermaLink="true">https://swastik.codes/blog/mqtt-client-online/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>MQTT</category><category>IoT</category><category>Testing</category>
<description>Connect to an MQTT broker over TCP, TLS or WebSocket from your browser. Subscribe, publish with QoS and retain, set a last will and MQTT 5 properties.</description>
<content:encoded><![CDATA[<p>An online MQTT client lets you connect to a broker, subscribe to topics and publish messages without installing a desktop tool or a command-line client. In Swastik Codes you create an <strong>MQTT</strong> request, enter the broker URL (<code>mqtt://</code>, <code>mqtts://</code>, <code>ws://</code> or <code>wss://</code>), press <strong>Connect</strong>, add a topic filter on the <strong>Topics</strong> tab and publish from the <strong>Message</strong> tab. Both MQTT 3.1.1 and MQTT 5.0 are supported, including QoS 0, 1 and 2, retained messages, last will messages and MQTT 5 properties.</p>
<p>This guide walks through that flow using the public Eclipse Mosquitto test broker, <code>test.mosquitto.org</code>, and then covers what to check on your own broker.</p>
<h2 id="what-you-need">What you need</h2>
<ul><li>A broker reachable from the public internet. The Swastik Codes web app does not connect from your browser tab directly: browsers cannot open raw TCP sockets, so the swastik.codes server opens the MQTT connection on your behalf and relays packets to your tab. That relay refuses private, loopback and other reserved addresses, so a broker on <code>localhost</code> or <code>10.0.0.5</code> cannot be reached from the web app. The Swastik Codes <a href="https://swastik.codes/download/">desktop app</a> for Windows and Linux connects from your own machine, so it reaches local brokers.</li><li>The broker's credentials, if it needs them: username and password, a client certificate, or MQTT 5 enhanced authentication.</li><li>A free Swastik Codes account, because protocol connections run through the server.</li></ul>
<p>For a first try, <code>test.mosquitto.org</code> accepts anonymous connections on port 1883 (plain MQTT over TCP) and port 8080 (MQTT over unencrypted WebSocket). It is a shared public test broker: anyone can read what you publish, so never send real data there, and use a unique topic prefix such as <code>swc-demo/&lt;your-name&gt;/</code> so you do not collide with other people.</p>
<h2 id="step-1-create-an-mqtt-request-and-connect">Step 1: create an MQTT request and connect</h2>
<ol><li>Open <a href="https://swastik.codes/app">the app</a> and sign in.</li><li>Click <strong>+</strong> in the tab bar and choose <strong>MQTT</strong> from the <strong>New request</strong> menu.</li><li>Enter the broker URL. The scheme picks the transport:
<ul><li><code>mqtt://host:1883</code> or <code>tcp://host:1883</code> for plain MQTT over TCP,</li><li><code>mqtts://host:8883</code> (also <code>ssl://</code> or <code>tls://</code>) for MQTT over TLS,</li><li><code>ws://host:port/path</code> or <code>wss://host:port/path</code> for MQTT over WebSocket, with the <code>mqtt</code> subprotocol.</li></ul></li><li>Choose the version next to the URL: <strong>MQTT 3.1.1</strong> or <strong>MQTT 5.0</strong>.</li><li>Press <strong>Connect</strong>.</li></ol>
<p>For the demo, enter <code>mqtt://test.mosquitto.org:1883</code>, or <code>ws://test.mosquitto.org:8080</code> to try the WebSocket transport.</p>
<p>The <strong>Settings</strong> tab holds the connection options: <strong>Client ID</strong> (leave it empty for a random id per connection, or generate one), <strong>Keep-alive (s)</strong>, <strong>Clean session</strong> (called <strong>Clean start</strong> in MQTT 5), <strong>Connection timeout (ms)</strong>, automatic reconnection with a number of attempts and a delay, and TLS options: <strong>Use TLS (mqtts / wss)</strong>, <strong>Verify the broker certificate</strong>, <strong>Server name (SNI)</strong>, and PEM fields for a CA certificate, client certificate and client key for mutual TLS. Username and password live on the <strong>Authorization</strong> tab.</p>
<h2 id="step-2-subscribe-to-topics">Step 2: subscribe to topics</h2>
<p>Open the <strong>Topics</strong> tab, type a topic filter and click <strong>Subscribe</strong>. MQTT filters use two wildcards:</p>
<ul><li><code>+</code> matches exactly one level: <code>sensors/+/temp</code> matches <code>sensors/kitchen/temp</code> but not <code>sensors/kitchen/1/temp</code>.</li><li><code>#</code> matches any number of levels and must come last: <code>swc-demo/alice/#</code> matches everything under that prefix.</li></ul>
<p>Pick the QoS for each subscription. The broker delivers each message at the lower of the publisher's QoS and the subscription's QoS. Each subscription has a switch to unsubscribe and subscribe again without deleting it, and you can add a description to remember what a topic is for.</p>
<p>With MQTT 5, each subscription also has subscription options:</p>
<ul><li><strong>No local</strong>: do not receive messages you published yourself.</li><li><strong>Retain as published</strong>: keep the retain flag as the publisher set it instead of clearing it.</li><li><strong>Retained messages</strong>: <strong>Send on subscribe</strong>, <strong>Only for new subscriptions</strong> or <strong>Never send</strong>, which control whether the broker replays retained messages when you subscribe.</li><li><strong>Subscription id</strong>: a number the broker attaches to every message delivered through this subscription.</li></ul>
<p>Incoming messages appear in the log with their topic, QoS, retain flag and payload, and you can filter the log by topic.</p>
<h2 id="step-3-publish-messages">Step 3: publish messages</h2>
<p>On the <strong>Message</strong> tab, enter a topic, choose the QoS (<strong>QoS 0</strong>, <strong>QoS 1</strong> or <strong>QoS 2</strong>), set <strong>Retain</strong> if you want the broker to keep the message for future subscribers, choose a payload format and write the payload. Formats are Text, JSON, XML, Hex and Base64; Hex and Base64 let you publish binary payloads such as Protobuf or CBOR. Press <strong>Publish</strong>.</p>
<p>Try this with the demo broker: subscribe to <code>swc-demo/alice/#</code>, then publish to <code>swc-demo/alice/kitchen/temp</code>:</p>
<div class="code"><div class="code-head"><span>JSON</span></div><pre tabindex="0"><code class="language-json">{ &quot;celsius&quot;: 21.5, &quot;at&quot;: &quot;{{$isoTimestamp}}&quot; }</code></pre></div>
<p>You should see your own message come back through the subscription. Variables and dynamic values are resolved before publishing.</p>
<h3 id="what-the-qos-levels-mean">What the QoS levels mean</h3>
<ul><li><strong>QoS 0</strong>, at most once: fire and forget. Fine for frequent telemetry where a lost reading does not matter.</li><li><strong>QoS 1</strong>, at least once: the receiver acknowledges with PUBACK and the sender retries until it does. Duplicates are possible, so consumers should be idempotent.</li><li><strong>QoS 2</strong>, exactly once: a four-step handshake (PUBLISH, PUBREC, PUBREL, PUBCOMP). Slower, but no duplicates between two hops.</li></ul>
<p>Swastik Codes performs the full handshake for each level in both directions, so you can see how your broker and consumers behave with each.</p>
<h3 id="retained-messages">Retained messages</h3>
<p>A retained message is the last known value of a topic. When you publish with <strong>Retain</strong> on, the broker stores the message and sends it to every new subscriber immediately. To clear a retained message, publish an empty payload with retain on to the same topic. This is a common source of confusion: a dashboard shows stale data because somebody once published a retained message to the topic.</p>
<h2 id="step-4-last-will-and-mqtt-5-properties">Step 4: last will and MQTT 5 properties</h2>
<p>The <strong>Last will</strong> tab configures the message the broker publishes on your behalf if the client disconnects without a proper DISCONNECT, for example on a network failure. Turn on <strong>Send a last-will message</strong> and set a topic, payload, QoS and retain flag. A common pattern is a retained <code>status</code> topic: the device publishes <code>online</code> after connecting, and the will publishes <code>offline</code>. In MQTT 5 the will can also have a <strong>Delay interval (s)</strong> and message properties.</p>
<p>With <strong>MQTT 5.0</strong> selected, the <strong>Properties</strong> tab adds CONNECT properties: <strong>Session expiry (s)</strong>, <strong>Receive maximum</strong>, <strong>Maximum packet size (bytes)</strong>, <strong>Topic alias maximum</strong>, <strong>Request response information</strong>, <strong>Request problem information</strong>, user properties, and <strong>Authentication method</strong> and <strong>Authentication data</strong> for enhanced auth. Individual publishes get message properties under <strong>MQTT 5 properties</strong>: <strong>Content type</strong>, <strong>Response topic</strong>, <strong>Correlation data</strong>, <strong>Message expiry (s)</strong>, <strong>Topic alias</strong>, <strong>Payload format indicator (UTF-8)</strong> and user properties.</p>
<p>Response topic and correlation data are how request/response works over MQTT 5. Publish a command with <code>Response topic: swc-demo/alice/replies</code> and some correlation data, subscribe to <code>swc-demo/alice/replies</code>, and check that your service answers with the same correlation data.</p>
<h2 id="step-5-save-messages">Step 5: save messages</h2>
<p>Click <strong>Save</strong> next to <strong>Publish</strong> to keep the current message, including its topic, QoS, retain flag and format, with the request. Saved messages are listed on the <strong>Saved</strong> tab and can be published again with one click while connected. Save the request to a collection with Ctrl+S or Cmd+S and everyone in the workspace gets the broker URL, subscriptions, will and saved messages.</p>
<h2 id="run-your-own-broker">Run your own broker</h2>
<p>For a private test broker, Eclipse Mosquitto is a small and well-documented choice. A minimal <code>mosquitto.conf</code> with password authentication and both TCP and WebSocket listeners:</p>
<div class="code"><div class="code-head"><span>Text</span></div><pre tabindex="0"><code class="language-text"># mosquitto.conf
listener 1883
protocol mqtt

listener 8083
protocol websockets

allow_anonymous false
password_file /mosquitto/config/passwd</code></pre></div>
<p>Create the password file and start it with Docker:</p>
<div class="code"><div class="code-head"><span>Shell</span></div><pre tabindex="0"><code class="language-bash">docker run --rm -v &quot;$PWD:/mosquitto/config&quot; eclipse-mosquitto \
  mosquitto_passwd -c -b /mosquitto/config/passwd tester 's3cret'

docker run -d --name mosquitto -p 1883:1883 -p 8083:8083 \
  -v &quot;$PWD:/mosquitto/config&quot; eclipse-mosquitto</code></pre></div>
<p>Put TLS in front of it (a <code>listener 8883</code> with <code>certfile</code> and <code>keyfile</code>, or a TLS-terminating proxy for the WebSocket listener) before exposing it on the internet. To see traffic flowing, run a small simulated device with the <code>mqtt</code> package for Node.js:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-js">// device.mjs  (npm install mqtt)
import mqtt from 'mqtt';

const client = mqtt.connect('mqtts://broker.example.com:8883', {
  username: 'tester',
  password: process.env.MQTT_PASSWORD,
  protocolVersion: 5,
  will: { topic: 'devices/sensor-1/status', payload: 'offline', qos: 1, retain: true },
});

client.on('connect', () =&gt; {
  client.publish('devices/sensor-1/status', 'online', { qos: 1, retain: true });
  client.subscribe('devices/sensor-1/commands', { qos: 1 });
  setInterval(() =&gt; {
    const reading = { celsius: 20 + Math.random() * 5, at: new Date().toISOString() };
    client.publish('devices/sensor-1/telemetry', JSON.stringify(reading), { qos: 0 });
  }, 5000);
});

client.on('message', (topic, payload) =&gt; {
  console.log('command on', topic, payload.toString());
});</code></pre></div>
<p>Now connect from Swastik Codes, subscribe to <code>devices/#</code>, and publish to <code>devices/sensor-1/commands</code>. Stop the device process abruptly and watch the broker publish the <code>offline</code> will.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<p><strong>CONNACK says &quot;not authorized&quot; or &quot;bad user name or password&quot;.</strong> Check the <strong>Authorization</strong> tab. On MQTT 5 the reason code in the log tells you exactly which check failed.</p>
<p><strong>The connection is refused for a private address.</strong> The relay blocks private and loopback addresses. Use a broker with a public hostname.</p>
<p><strong>TLS errors on <code>mqtts://</code>.</strong> Make sure the port really serves TLS (8883 by convention), set <strong>Server name (SNI)</strong> if you connect by IP, and add the CA certificate if the broker uses a private CA.</p>
<p><strong>WebSocket connection fails.</strong> Most brokers serve MQTT over WebSocket on a specific path, often <code>/mqtt</code>. Include it in the URL.</p>
<p><strong>I get disconnected right after connecting.</strong> Two clients with the same client id kick each other off. Leave <strong>Client ID</strong> empty for a random one.</p>
<p><strong>Old messages arrive the moment I subscribe.</strong> They are retained messages. Clear them by publishing an empty retained payload, or set <strong>Retained messages</strong> to <strong>Never send</strong> on an MQTT 5 subscription.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="which-mqtt-versions-are-supported">Which MQTT versions are supported?</h3>
<p>MQTT 3.1.1 and MQTT 5.0. MQTT 5 adds reason codes, properties, subscription options, session expiry and enhanced authentication, and all of those are available in the editor.</p>
<h3 id="can-i-connect-over-websocket">Can I connect over WebSocket?</h3>
<p>Yes. Use a <code>ws://</code> or <code>wss://</code> URL and the client speaks MQTT over WebSocket with the <code>mqtt</code> subprotocol. Plain TCP (<code>mqtt://</code>) and TLS (<code>mqtts://</code>) work as well.</p>
<h3 id="can-i-publish-binary-payloads">Can I publish binary payloads?</h3>
<p>Yes. Choose Hex or Base64 as the payload format and the bytes are decoded before publishing.</p>
<h3 id="can-i-test-a-broker-on-my-local-network">Can I test a broker on my local network?</h3>
<p>Not from the web app, because the relay refuses private and loopback addresses. Use the <a href="https://swastik.codes/download/">desktop app</a>, which connects from your own computer, or expose a test broker on a public hostname, protected by a password and TLS.</p>
<h3 id="is-test-mosquitto-org-safe-to-use">Is test.mosquitto.org safe to use?</h3>
<p>It is fine for learning and quick checks, but it is public: anything you publish can be read by anyone. Use a unique topic prefix and never send credentials or personal data.</p>
<h2 id="try-it">Try it</h2>
<p>Open <a href="https://swastik.codes/app">Swastik Codes</a>, create an MQTT request, connect to <code>mqtt://test.mosquitto.org:1883</code> and subscribe to your own topic prefix. You will need a <a href="https://swastik.codes/signup">free account</a> to connect. The <a href="https://swastik.codes/features/mqtt-client/">MQTT client feature page</a> lists every option. If your system also pushes data to browsers, see <a href="https://swastik.codes/blog/test-server-sent-events/">how to test Server-Sent Events</a> and <a href="https://swastik.codes/blog/test-websocket-api-online/">how to test a WebSocket API</a>.</p>]]></content:encoded>
</item>
<item>
<title>OAuth 2.0 in an API client: get and use access tokens</title>
<link>https://swastik.codes/blog/oauth2-in-api-client/</link>
<guid isPermaLink="true">https://swastik.codes/blog/oauth2-in-api-client/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>OAuth 2.0</category><category>Authentication</category><category>Tutorial</category>
<description>Get OAuth 2.0 access tokens in a browser API client: authorization code with PKCE, client credentials, password, implicit, refresh and callback URL setup.</description>
<content:encoded><![CDATA[<p>To call an API protected by OAuth 2.0 from an API client, you configure the client with the provider's authorization and token URLs, your client ID (and secret, for confidential clients), the scopes you need and a callback URL. The client then runs the grant, receives an access token, and attaches it to your requests as <code>Authorization: Bearer &lt;token&gt;</code>. In Swastik Codes you do this in the <strong>Authorization</strong> tab: choose <strong>OAuth 2.0</strong>, fill in <strong>Configure new token</strong>, and press <strong>Get New Access Token</strong>.</p>
<p>This guide explains each grant type, what to register with your provider, how to share a token across a whole collection, and how to fix the errors you are most likely to meet.</p>
<h2 id="before-you-start">Before you start</h2>
<ul><li><strong>Register a client with your provider</strong> (Auth0, Okta, Keycloak, Azure AD / Entra ID, Google, or your own server). You will get a client ID and, for confidential clients, a client secret.</li><li><strong>Register the callback URL.</strong> For the Swastik Codes web app it is:</li></ul>
<div class="code"><div class="code-head"><span>Text</span></div><pre tabindex="0"><code class="language-text">https://swastik.codes/oauth/callback</code></pre></div>
<p>The authorization code, PKCE and implicit grants redirect the popup there. Providers compare redirect URIs exactly, so copy it character for character.</p>
<ul><li><strong>Have a free account.</strong> Token requests and API calls are sent through the swastik.codes server, which needs you signed in. The server also refuses private and loopback addresses, so the provider's token endpoint must be publicly reachable. An identity server on <code>localhost</code> will not work from the web app; use the <a href="https://swastik.codes/download/">desktop app</a> for that.</li></ul>
<h2 id="step-1-choose-where-the-auth-lives">Step 1: choose where the auth lives</h2>
<p>Authorization can be set on a request, a folder or a collection. Requests default to <strong>Inherit</strong>, which uses the nearest folder or collection auth; the Authorization tab tells you which one is in effect (&quot;This request uses OAuth 2.0 from …&quot;).</p>
<p>For an API where every endpoint needs the same token, configure OAuth 2.0 once on the collection (open the collection and select its <strong>Authorization</strong> tab) and leave the requests on Inherit. Override it on a folder or request only where an endpoint needs something different, such as <strong>No Auth</strong> for a public health check.</p>
<h2 id="step-2-configure-the-grant">Step 2: configure the grant</h2>
<p>Open the Authorization tab, choose <strong>OAuth 2.0</strong> and fill in the <strong>Configure new token</strong> section. The fields depend on the <strong>Grant type</strong>.</p>
<h3 id="authorization-code-with-pkce-for-most-user-facing-apis">Authorization Code (With PKCE) – for most user-facing APIs</h3>
<p>This is the grant to use when a user signs in at the provider and approves access. PKCE protects the code exchange and is required by OAuth 2.1 and by many providers for public clients.</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Field</th><th>Example (Keycloak realm <code>demo</code>)</th></tr></thead><tbody><tr><td>Callback URL</td><td><code>https://swastik.codes/oauth/callback</code></td></tr><tr><td>Auth URL</td><td><code>https://id.example.com/realms/demo/protocol/openid-connect/auth</code></td></tr><tr><td>Access Token URL</td><td><code>https://id.example.com/realms/demo/protocol/openid-connect/token</code></td></tr><tr><td>Client ID</td><td><code>api-client</code></td></tr><tr><td>Client Secret</td><td>empty for a public client</td></tr><tr><td>Code Challenge Method</td><td>SHA-256</td></tr><tr><td>Code Verifier</td><td>leave empty to generate one</td></tr><tr><td>Scope</td><td><code>openid profile orders:read</code></td></tr><tr><td>State</td><td>optional; a random value is used when empty</td></tr></tbody></table></div>
<p>Press <strong>Get New Access Token</strong>. A popup opens at the provider; sign in and approve. The provider redirects the popup to the callback URL with a code, the app checks the <code>state</code>, and the code is exchanged for tokens. Allow popups for swastik.codes if your browser blocks it.</p>
<p>For Google the URLs are <code>https://accounts.google.com/o/oauth2/v2/auth</code> and <code>https://oauth2.googleapis.com/token</code>; for other providers, look for their OpenID Connect discovery document (<code>/.well-known/openid-configuration</code>), which lists <code>authorization_endpoint</code> and <code>token_endpoint</code>.</p>
<h3 id="authorization-code-without-pkce">Authorization Code (without PKCE)</h3>
<p>The same flow for confidential clients that do not use PKCE. Prefer the PKCE variant when the provider supports it.</p>
<h3 id="client-credentials-for-service-to-service-apis">Client Credentials – for service-to-service APIs</h3>
<p>No user and no popup: the client authenticates as itself. Fill in <strong>Access Token URL</strong>, <strong>Client ID</strong>, <strong>Client Secret</strong> and <strong>Scope</strong>, then press <strong>Get New Access Token</strong>. This is the usual grant for machine APIs and for running a collection on a schedule in a <a href="https://swastik.codes/features/api-monitoring/">monitor</a>.</p>
<h3 id="password-credentials-legacy-only">Password Credentials – legacy only</h3>
<p>The client sends a username and password directly to the token endpoint. OAuth 2.1 removes this grant and many providers disable it, but older internal APIs still use it. Fill in <strong>Username</strong> and <strong>Password</strong> in addition to the token URL and client details.</p>
<h3 id="implicit-legacy-only">Implicit – legacy only</h3>
<p>The token is returned directly in the redirect, without a code exchange. It is also removed in OAuth 2.1. It is supported for older providers; for anything new, use the authorization code grant with PKCE.</p>
<h3 id="client-authentication-and-extra-parameters">Client authentication and extra parameters</h3>
<p><strong>Client Authentication</strong> decides how the client ID and secret reach the token endpoint: <strong>Send as Basic Auth header</strong> (the default) or <strong>Send client credentials in body</strong>. If the provider answers <code>invalid_client</code> although the secret is right, try the other option.</p>
<p>Under the advanced fields you can set <strong>Audience</strong> and <strong>Resource</strong> (Auth0 and resource-indicator providers need these), a separate <strong>Refresh Token URL</strong>, and two tables of extra parameters: <strong>Extra authorization request parameters</strong> (for example <code>prompt=consent</code> or <code>access_type=offline</code> for Google, to get a refresh token) and <strong>Extra token request parameters</strong>, where each row can be sent in the request body, a header or the URL.</p>
<h2 id="step-3-use-the-token">Step 3: use the token</h2>
<p>When the token arrives it is shown in a dialog; press <strong>Use Token</strong>. The <strong>Current token</strong> section then holds the access token and controls how it is sent:</p>
<ul><li><strong>Header Prefix</strong> – <code>Bearer</code> by default.</li><li><strong>Add token to</strong> – <strong>Request Headers</strong> (<code>Authorization: Bearer …</code>) or <strong>Request URL</strong> for APIs that expect it as a query parameter.</li><li><strong>Use Token Type</strong> – when the provider also returned an OpenID Connect ID token, choose whether to send the access token or the ID token.</li></ul>
<p>Send any request in the collection and it carries the token. To check, open the console: it shows the request headers that were actually sent.</p>
<h3 id="keep-tokens-out-of-shared-documents">Keep tokens out of shared documents</h3>
<p>Fields accept variables, so the client secret can be <code>{{clientSecret}}</code> with the value stored in an environment. Variable <em>current</em> values stay on your device and are not synced, so a secret kept as a current value is not shared with your team. The vault keeps encrypted secrets that resolve only for hosts you allow. See <a href="https://swastik.codes/features/environments-variables/">environments and variables</a>.</p>
<h2 id="step-4-refresh-and-manage-tokens">Step 4: refresh and manage tokens</h2>
<p>Access tokens expire. If the provider returned a refresh token:</p>
<ul><li><strong>Auto-refresh token</strong> (on by default) swaps an expired token for a fresh one through the refresh grant just before a request is sent.</li><li><strong>Refresh token</strong> in the Current token section refreshes on demand.</li></ul>
<p><strong>Manage</strong> opens the list of saved tokens. Each token has a name (from <strong>Token Name</strong>), its expiry and its refresh token, and you can use, refresh or delete it, or clear all expired ones. Expired tokens are marked in the token selector.</p>
<h2 id="getting-a-token-in-a-script-instead">Getting a token in a script instead</h2>
<p>Sometimes a token comes from a non-standard login endpoint. A pre-request script with <code>swc.sendRequest</code> can fetch it and store it:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// Collection pre-request script
const expiresAt = swc.environment.get('tokenExpiresAt');
if (!expiresAt || Date.now() &gt; Number(expiresAt)) {
  swc.sendRequest({
    url: swc.environment.get('loginUrl'),
    method: 'POST',
    header: [{ key: 'Content-Type', value: 'application/json' }],
    body: { mode: 'raw', raw: JSON.stringify({
      username: swc.environment.get('user'),
      password: swc.environment.get('password')
    }) }
  }, function (err, res) {
    if (err) { console.log('login failed', err); return; }
    const body = res.json();
    swc.environment.set('token', body.access_token);
    swc.environment.set('tokenExpiresAt', Date.now() + (body.expires_in - 30) * 1000);
  });
}</code></pre></div>
<p>Then set the collection's auth to <strong>Bearer Token</strong> with the value <code>{{token}}</code>. More script patterns are in <a href="https://swastik.codes/blog/api-tests-with-pm-scripts/">writing API tests with swc.* scripts</a>.</p>
<h2 id="other-schemes-in-the-same-tab">Other schemes in the same tab</h2>
<p>OAuth 2.0 is one of several authorization types. The same tab handles API Key, Bearer Token, JWT (signed for you), Basic, Digest, OAuth 1.0, Hawk, AWS Signature v4, NTLM, Akamai EdgeGrid and ASAP. See the <a href="https://swastik.codes/features/authorization/">authorization feature page</a> for the full list.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<p><strong><code>redirect_uri_mismatch</code> or &quot;invalid redirect&quot;.</strong> The callback URL registered with the provider must equal <code>https://swastik.codes/oauth/callback</code> exactly, including the scheme and with no trailing slash.</p>
<p><strong>&quot;Popup blocked&quot;.</strong> Allow popups for swastik.codes and press Get New Access Token again.</p>
<p><strong>&quot;OAuth state mismatch&quot;.</strong> An old popup answered after a newer attempt started. Close stray popups and retry.</p>
<p><strong><code>invalid_client</code>.</strong> Wrong secret, or the provider expects the credentials in the other place: switch <strong>Client Authentication</strong> between the Basic Auth header and the body.</p>
<p><strong><code>invalid_grant</code> on refresh.</strong> The refresh token was revoked or rotated. Get a new token with the original grant.</p>
<p><strong>The token request never reaches the provider.</strong> If the token URL is on a private network or <code>localhost</code>, the swastik.codes server refuses it. Use a publicly reachable identity server.</p>
<p><strong>Requests still return 401.</strong> Check the scopes and audience the API expects, and decode the token (the JWT payload is base64) to see what it actually contains.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="what-callback-url-should-i-register-for-swastik-codes">What callback URL should I register for Swastik Codes?</h3>
<p><code>https://swastik.codes/oauth/callback</code> for the web app.</p>
<h3 id="is-pkce-supported">Is PKCE supported?</h3>
<p>Yes. Choose <strong>Authorization Code (With PKCE)</strong>. The code verifier is generated automatically when the field is empty, and the challenge method can be SHA-256 (recommended) or plain.</p>
<h3 id="can-one-token-be-shared-by-every-request-in-a-collection">Can one token be shared by every request in a collection?</h3>
<p>Yes. Configure OAuth 2.0 on the collection or a folder and leave the requests on Inherit.</p>
<h3 id="can-i-test-an-mcp-server-that-uses-oauth">Can I test an MCP server that uses OAuth?</h3>
<p>Yes. MCP requests include a discovery debugger that walks through protected resource metadata, authorization server metadata and dynamic client registration before getting a token. See <a href="https://swastik.codes/blog/test-mcp-server/">testing an MCP server</a>.</p>
<h3 id="are-my-client-secrets-synced">Are my client secrets synced?</h3>
<p>Values you keep as a variable's current value stay on your device. Values you type directly into a saved request's auth fields are part of that request and are stored with the collection, so prefer variables or the vault for secrets.</p>
<h2 id="try-it">Try it</h2>
<p><a href="https://swastik.codes/app">Open the app</a>, set OAuth 2.0 on a collection and get your first token; token requests need a <a href="https://swastik.codes/signup">free account</a>. The <a href="https://swastik.codes/features/authorization/">authorization feature page</a> lists every supported scheme, and <a href="https://swastik.codes/features/environments-variables/">environments and variables</a> explains how to keep credentials per environment. If you are moving from another client, <a href="https://swastik.codes/blog/import-postman-collection/">importing a Postman collection</a> keeps its auth settings.</p>]]></content:encoded>
</item>
<item>
<title>Write API tests with swc.* scripts (Postman pm.* compatible)</title>
<link>https://swastik.codes/blog/api-tests-with-pm-scripts/</link>
<guid isPermaLink="true">https://swastik.codes/blog/api-tests-with-pm-scripts/</guid>
<pubDate>Thu, 22 Oct 2026 03:30:00 GMT</pubDate>
<dc:creator>Divakar</dc:creator>
<category>API testing</category><category>Scripts</category><category>JavaScript</category><category>Tutorial</category>
<description>swc.test and swc.expect examples for status, headers, JSON bodies, schemas, variables, chained requests and runs. Postman pm.test scripts run unchanged.</description>
<content:encoded><![CDATA[<p>API tests in the Postman style are small JavaScript snippets that run after a response arrives. Each <code>swc.test('name', fn)</code> call is one named check. Inside it you assert with <code>swc.expect(...)</code> or with <code>swc.response.to...</code> helpers, and a thrown assertion marks that test as failed. A pre-request script runs before the request is sent and is used to prepare data: timestamps, signatures, tokens.</p>
<p>Swastik Codes runs these scripts in a sandboxed JavaScript engine that implements Postman's documented <code>pm.*</code> API, so most scripts written for Postman work unchanged. The examples use <code>swc.*</code>, Swastik Codes' name for the same API: <code>swc</code> and <code>pm</code> are one object, so every <code>swc.</code> below can also be written <code>pm.</code>, and imported <code>pm.*</code> scripts can be converted to <code>swc.*</code> in one click. This guide is a set of working examples you can paste into the app, from the basic status check to chained requests and runner control.</p>
<h2 id="where-scripts-live">Where scripts live</h2>
<p>Open a request in <a href="https://swastik.codes/app">the app</a> and select the <strong>Scripts</strong> tab. It has two editors:</p>
<ul><li><strong>Pre-request</strong> – runs before the request is sent.</li><li><strong>Post-response</strong> – runs after the response arrives. This is where tests go.</li></ul>
<p>Collections and folders have a <strong>Scripts</strong> tab too. When a request is sent, the pre-request scripts run in order collection → folder(s) → request, then the request is sent, then the post-response scripts run in the same order. Put shared logic (an auth header, a common status check) at the collection level and request-specific checks on the request.</p>
<p>The editor offers snippets for common tasks, such as <em>Status code is 200</em>, <em>JSON value check</em>, <em>Validate JSON schema</em> and <em>Save a token from the response</em>, so you rarely start from an empty file.</p>
<p>Results appear in the response panel's <strong>Test Results</strong> tab, with a passed/total count, and <code>console.log</code> output goes to the console panel.</p>
<p>Sending requires a free account, because the web app sends them through the swastik.codes server; that server also refuses private and loopback addresses, so the API must be publicly reachable. The examples below use <code>https://jsonplaceholder.typicode.com</code> and <code>https://httpbin.org</code>, which you can call right away.</p>
<h2 id="the-basics-status-time-headers">The basics: status, time, headers</h2>
<p>Create a GET request to <code>https://jsonplaceholder.typicode.com/users/1</code> and add this post-response script:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('status is 200', function () {
  swc.response.to.have.status(200);
});

swc.test('responds within 1500 ms', function () {
  swc.expect(swc.response.responseTime).to.be.below(1500);
});

swc.test('returns JSON', function () {
  swc.response.to.have.header('Content-Type');
  swc.expect(swc.response.headers.get('Content-Type')).to.include('application/json');
});</code></pre></div>
<p>Other status helpers read well in tests: <code>swc.response.to.be.ok</code> (exactly 200), <code>swc.response.to.be.success</code> (any 2XX), <code>swc.response.to.be.clientError</code>, <code>swc.response.to.be.notFound</code>, <code>swc.response.to.be.unauthorized</code> and <code>swc.response.to.be.rateLimited</code>.</p>
<h2 id="asserting-on-the-json-body">Asserting on the JSON body</h2>
<p><code>swc.response.json()</code> parses the body. From there it is ordinary chai:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const user = swc.response.json();

swc.test('user has the expected identity', function () {
  swc.expect(user.id).to.eql(1);
  swc.expect(user.email).to.match(/@/);
  swc.expect(user).to.have.property('address');
  swc.expect(user.address.geo).to.have.all.keys('lat', 'lng');
});

swc.test('company name is a non-empty string', function () {
  swc.expect(user.company.name).to.be.a('string').and.not.empty;
});</code></pre></div>
<p>For quick checks without parsing, <code>jsonBody</code> takes a lodash-style path:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">swc.test('city is present', function () {
  swc.response.to.have.jsonBody('address.city');
});

swc.test('username is Bret', function () {
  swc.response.to.have.jsonBody('username', 'Bret');
});</code></pre></div>
<p>And <code>swc.response.to.have.body(/Leanne/)</code> checks the raw text against a regular expression.</p>
<h2 id="validating-against-a-json-schema">Validating against a JSON Schema</h2>
<p>Checking individual fields catches the fields you thought of. A schema catches the rest: wrong types, missing required fields, unexpected nulls. <code>jsonSchema</code> validates with Ajv:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const userSchema = {
  type: 'object',
  required: ['id', 'name', 'email', 'address'],
  properties: {
    id: { type: 'integer', minimum: 1 },
    name: { type: 'string' },
    email: { type: 'string' },
    address: {
      type: 'object',
      required: ['city', 'zipcode'],
      properties: {
        city: { type: 'string' },
        zipcode: { type: 'string' }
      }
    }
  }
};

swc.test('matches the user schema', function () {
  swc.response.to.have.jsonSchema(userSchema);
});</code></pre></div>
<p>If a test fails, the message lists every schema error, because Ajv runs with <code>allErrors</code>. Older scripts that use <code>tv4.validate(data, schema)</code> also work: <code>tv4</code> is one of the bundled libraries.</p>
<h2 id="variables-and-scopes">Variables and scopes</h2>
<p>Variables are written as <code>{{name}}</code> in URLs, headers and bodies, and read or written in scripts:</p>
<div class="table-wrap" tabindex="0"><table><thead><tr><th>Scope</th><th>Script API</th><th>Typical use</th></tr></thead><tbody><tr><td>Global</td><td><code>swc.globals</code></td><td>values shared by every collection in the workspace</td></tr><tr><td>Collection</td><td><code>swc.collectionVariables</code></td><td>base URLs and ids belonging to one API</td></tr><tr><td>Environment</td><td><code>swc.environment</code></td><td>per-environment values: staging vs production hosts, credentials</td></tr><tr><td>Data</td><td><code>swc.iterationData</code></td><td>the current row of a data file in a collection run (read-only)</td></tr><tr><td>Local</td><td><code>swc.variables</code></td><td>values for this request or run only</td></tr></tbody></table></div>
<p><code>swc.variables.get('name')</code> resolves through all scopes, with the narrowest winning (local, then data, environment, collection, global). <code>swc.variables.replaceIn('{{baseUrl}}/users')</code> expands a string the same way.</p>
<p>A common pattern is saving a value from one response for the next request:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// Post-response script of POST /posts
const created = swc.response.json();

swc.test('post was created', function () {
  swc.response.to.have.status(201);
  swc.expect(created.id).to.be.a('number');
});

swc.collectionVariables.set('postId', created.id);</code></pre></div>
<p>The next request can then call <code>GET https://jsonplaceholder.typicode.com/posts/{{postId}}</code>. (JSONPlaceholder fakes writes, so that id will not really exist there, but the pattern is the same on a real API.)</p>
<h2 id="pre-request-scripts">Pre-request scripts</h2>
<p>Pre-request scripts prepare the request. They can change it directly through <code>swc.request</code>:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// Pre-request: add a timestamp and a request id
swc.variables.set('now', new Date().toISOString());
swc.request.headers.upsert({ key: 'X-Request-Id', value: require('uuid').v4() });</code></pre></div>
<p>Signing a request with an HMAC is a typical job for <code>crypto-js</code>:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const CryptoJS = require('crypto-js');
const secret = swc.environment.get('signingSecret');
const body = swc.request.body ? swc.request.body.raw || '' : '';
const ts = Math.floor(Date.now() / 1000).toString();
const signature = CryptoJS.HmacSHA256(ts + '.' + body, secret).toString(CryptoJS.enc.Hex);

swc.request.headers.upsert({ key: 'X-Timestamp', value: ts });
swc.request.headers.upsert({ key: 'X-Signature', value: 'sha256=' + signature });</code></pre></div>
<p>If a pre-request script throws, the request is not sent and the error is shown, which is how the desktop Postman app behaves. In a collection run the request is still sent, as Newman does.</p>
<h2 id="calling-another-endpoint-from-a-script">Calling another endpoint from a script</h2>
<p><code>swc.sendRequest</code> sends an extra request from inside a script. It accepts a URL string or a request object, and works with a callback or as a promise:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// Pre-request: fetch a token if we do not have one yet
if (!swc.environment.get('token')) {
  swc.sendRequest({
    url: 'https://httpbin.org/post',
    method: 'POST',
    header: [{ key: 'Content-Type', value: 'application/json' }],
    body: { mode: 'raw', raw: JSON.stringify({ token: 'demo-token' }) }
  }, function (err, res) {
    if (err) { console.log(err); return; }
    swc.environment.set('token', res.json().json.token);
  });
}</code></pre></div>
<p>httpbin echoes the JSON you post under <code>json</code>, so this saves <code>demo-token</code>. Against a real identity provider you would post credentials and read <code>access_token</code>. For OAuth 2.0 it is usually simpler to use the built-in OAuth 2.0 authorization type instead; see <a href="https://swastik.codes/blog/oauth2-in-api-client/">OAuth 2.0 in an API client</a>.</p>
<p>Requests made with <code>swc.sendRequest</code> appear in the console as <code>swc.sendRequest</code> entries, so you can see exactly what the script sent and received.</p>
<h2 id="controlling-a-collection-run">Controlling a collection run</h2>
<p>When requests run in the <a href="https://swastik.codes/features/collection-runner/">collection runner</a> (or in a monitor), scripts can change the order:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// Post-response: stop polling once the job is done
const job = swc.response.json();
if (job.status === 'done') {
  swc.execution.setNextRequest('Download result');
} else {
  swc.execution.setNextRequest('Check job status'); // run this request again
}</code></pre></div>
<p><code>swc.execution.setNextRequest(null)</code> ends the current iteration. <code>swc.execution.skipRequest()</code>, called from a pre-request script, skips the send:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">// Pre-request: only run on staging
if (swc.environment.get('env') !== 'staging') {
  swc.execution.skipRequest();
}</code></pre></div>
<p>Skipped requests are counted separately in the run results. The legacy <code>postman.setNextRequest('name')</code> spelling also works.</p>
<h2 id="bundled-libraries">Bundled libraries</h2>
<p><code>require()</code> gives access to these libraries without installing anything: <code>chai</code>, <code>lodash</code>, <code>moment</code>, <code>crypto-js</code>, <code>uuid</code>, <code>ajv</code>, <code>tv4</code>, <code>xml2js</code>, <code>cheerio</code>, <code>csv-parse/lib/sync</code>, <code>handlebars</code>, and Node-style modules such as <code>buffer</code>, <code>url</code>, <code>querystring</code>, <code>util</code>, <code>events</code>, <code>path</code>, <code>assert</code> and <code>crypto</code>. <code>atob</code> and <code>btoa</code> are available too.</p>
<p>Parsing XML, for example:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const xml2js = require('xml2js');
xml2js.parseString(swc.response.text(), function (err, result) {
  swc.test('XML parses', function () {
    swc.expect(err).to.be.null;
  });
});</code></pre></div>
<h2 id="visualizing-a-response">Visualizing a response</h2>
<p><code>swc.visualizer.set(template, data)</code> renders a Handlebars template in the response's <strong>Visualize</strong> view, which is handy for turning a JSON list into a readable table:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">const template = `
&lt;table&gt;
  &lt;tr&gt;&lt;th&gt;Name&lt;/th&gt;&lt;th&gt;Email&lt;/th&gt;&lt;/tr&gt;
  {{#each users}}
  &lt;tr&gt;&lt;td&gt;{{name}}&lt;/td&gt;&lt;td&gt;{{email}}&lt;/td&gt;&lt;/tr&gt;
  {{/each}}
&lt;/table&gt;`;

// GET https://jsonplaceholder.typicode.com/users
swc.visualizer.set(template, { users: swc.response.json() });</code></pre></div>
<h2 id="older-script-styles">Older script styles</h2>
<p>Collections that have been around for a while often contain legacy syntax. It still runs:</p>
<div class="code"><div class="code-head"><span>JavaScript</span></div><pre tabindex="0"><code class="language-javascript">tests['Status code is 200'] = responseCode.code === 200;
tests['Body mentions Leanne'] = responseBody.has('Leanne');
postman.setEnvironmentVariable('lastStatus', responseCode.code);</code></pre></div>
<p>Converting to <code>swc.test</code> is worth it when you next touch the script, because named tests with assertion messages are much easier to debug.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<p><strong><code>swc.response.json()</code> throws.</strong> The body is not JSON: check the status (an HTML error page is common) and the <code>Content-Type</code>. Test the status first, so the failure says what really happened.</p>
<p><strong>A variable is empty.</strong> Check which scope you set and which you read. <code>swc.environment.set</code> does nothing useful without an active environment; use <code>swc.collectionVariables</code> when the value belongs to the collection.</p>
<p><strong>Tests pass locally but fail in a run.</strong> In the runner, variable changes always carry to the next request within an iteration, but they carry into the next iteration only when <em>Persist variables between requests</em> is on; the order follows the collection unless scripts call <code>setNextRequest</code>.</p>
<p><strong>A module is not found.</strong> Only the bundled libraries listed above can be required.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="are-postman-test-scripts-compatible">Are Postman test scripts compatible?</h3>
<p>Scripts that use Postman's documented <code>pm.*</code> API (tests, expectations, variables, <code>sendRequest</code>, cookies, visualizer, execution control) and the legacy <code>tests[]</code>/<code>postman.*</code> globals are supported. Compatibility is built against the public documentation.</p>
<h3 id="can-tests-run-on-a-schedule">Can tests run on a schedule?</h3>
<p>Yes. Collections with tests can run on a cron schedule as <a href="https://swastik.codes/features/api-monitoring/">monitors</a>, with email or webhook alerts when they fail.</p>
<h3 id="can-i-run-the-same-tests-with-different-data">Can I run the same tests with different data?</h3>
<p>Yes. Attach a CSV or JSON file in the collection runner and read the current row with <code>swc.iterationData</code>. See <a href="https://swastik.codes/blog/data-driven-api-testing/">data-driven API testing</a>.</p>
<h3 id="do-async-tests-work">Do async tests work?</h3>
<p>Yes. A test function can return a promise, or take a <code>done</code> callback, and its result is recorded when it settles.</p>
<h2 id="try-it">Try it</h2>
<p><a href="https://swastik.codes/app">Open the app</a>, paste the first example into a request's Post-response script and send it with a <a href="https://swastik.codes/signup">free account</a>. The <a href="https://swastik.codes/features/api-testing/">API testing feature page</a> summarises the scripting support. When you have a few requests with tests, <a href="https://swastik.codes/blog/data-driven-api-testing/">run them as a collection with a data file</a>, or <a href="https://swastik.codes/blog/import-postman-collection/">import your existing Postman collection</a> and keep the scripts you already have.</p>]]></content:encoded>
</item>
</channel>
</rss>
