Imagine this scenario: You’re debugging an API call. The endpoint only works with production tokens, but your API testing tool insists on syncing everything to the cloud. Suddenly, your production credentials are sitting on someone else’s servers, risking potential exposure.
Most tools default to cloud-first, which can be limiting:
- Your secrets live in the cloud.
- Most features require an online account.
- Tests aren’t versioned with your code.
With Requestly, you get a local-first experience. Your API tests run on your machine, alongside your code, version-controlled just like the rest of your workflow. Your secrets and credentials stay on your device, safe and secure.
Local-first API testing with Requestly
With cloud-first API testing tools, your sensitive information lives in the cloud. One misconfigured account, one permission too broad, and you're risking exposing your sensitive information to the world. Requestly emphasizes local-first API testing that lets developers test APIs without depending on remote setups. You run everything right on your own machine, so you can intercept requests, mock responses, or reproduce tricky edge cases in seconds, all while keeping tokens and sensitive data safe. Because it’s all local, you get fast, reliable feedback that makes debugging smoother and development less painful.
This local-first approach means your tests can be stored next to your code, under Git. By keeping your Requestly assets under Git, you gain the benefits of collaborative development: teammates can review changes through pull requests, track history with commits, and roll back to previous versions if needed. This also fits naturally into existing workflows - your API test rules evolve alongside the code they validate.
In the following few sections of this article, you'll see Requestly in action. As an example of a practical API to test, we'll be using the GitHub REST API.
API Testing with Requestly
To follow along with this tutorial, you'll need the following dependencies on your system:
- Git
- Latest version of Node.js and npm.
- A GitHub Personal Access Token (PAT). Choose a repository where you have write access and give it read-write access for issues.
Setting up Requestly
To make things easy, I have already created a repo that contains the basic setup needed for this article. Clone the repo to your machine.
Install the dependencies:
npm install
You can either install Requestly as a browser extension or as a desktop app. You can read about the feature differences between these two versions here. For this tutorial, I'll use the desktop app, which you can download from here. So go ahead and install it.
Requestly works on the concept of Workspaces. The demo app already has a local workspace stored, which you need to load. Click on the Workspaces dropdown in the top-left corner and select + Add.
Choose the directory where you just cloned the app, and use requestly-demo
as the name of the workspace. This will load the workspace of the same name that is already in the directory.
Once the workspace is ready, navigate to the APIs tab, where you'll find the GitHub API collection. I have already prepared the requests so that we can focus on writing tests in this article. However, if you want to learn more about how to make requests, you can visit the documentation.
Note: Apart from REST APIs, Requestly also supports making GraphQL requests
There is also an Environment named "Dev" where you'll need to put your credentials:
-
token
: Your PAT -
owner
: Your GitHub username -
repo
: The name of the repo that you chose
We'll start with simple tests, then advance to schema validation and testing workflows. We'll see how to debug and handle errors, and finally, we'll mock a third-party API.
Writing Our First Test
If authentication breaks, your entire API is toast! So, we'll start by testing the Login endpoint. This will show you an example of how to handle authentication. If you click on the collection name and navigate to the Authorization tab, you'll see that the authorization type has been set to Bearer Token. For the token value, we're using {{token}}
, which retrieves the value from the environment variable.
Every endpoint in this collection inherits this authentication method, which means they'll automatically pass the token when you make the request.
Open the Login request and navigate to the Scripts tab. Requestly allows you to run scripts before the request is sent (Pre-request scripts), or after the response is received (Post-response scripts). For the purpose of writing API tests, we'll use the Post-response scripts.
Requestly uses Chai.js for assertions. It exposes the expect
method of Chai through rq.expect()
, which you can use to write assertions. Requestly also lets you access the request and response objects through rq.request
and rq.response
respectively, which can be used in the tests.
Our first test will make sure that the server returns 200 Ok
response. Let's start by creating a test. We want to check that rq.response.status
is equal to OK
. So, our test will look like this:
rq.test("Login is successful", () => { rq.expect(rq.response.status).to.equal("OK") })
Now, if you click on Send, Requestly will make the request and run the tests. You can see the result of the test in the Tests tab:
Requestly also allows you to check the status code using its name:
rq.response.to.be.ok
You can find all the status codes in the docs.
Testing Authentication
Now we know that the login request is successful, but how do we know that the login itself works correctly? The login endpoint also returns the logged-in user's data, which we can validate against the ground truth.
Here we can check that the login
property matches our username that we put into the owner
environment variable:
rq.test("Login returns valid data", () => { var body = JSON.parse(rq.response.body) rq.expect(body).to.have.property("login").that.is.a("string") rq.expect(body.login).to.equal(rq.environment.get("owner")) })
Send the requests, and if everything goes correctly, you should see the tests pass.
Schema Validation
APIs act as contracts between services, and if the structure (fields, types, nesting, required attributes) changes, consumers may break, even if the data looks valid. This is why it's vital to test that the response object has the correct schema.
In the List repos request, we're making a request to the /user/repos
endpoint, which fetches all the repos of the logged-in user. Let's assume that we are integrating this API with a frontend that expects the body to be an array of repos, and each repo must have id
, name
, private
, and owner
fields. The owner
field must have login
and id
fields, and the name
field must match the regex: /^\.?[a-z0-9-_\.]+$/i
.
In Requestly, you can use helpers from Chai to do this:
var body = JSON.parse(rq.response.body) rq.test("Fetched data has correct body", () => { // Check that body is an array rq.expect(body).to.be.an("array").that.is.not.empty for (let repo of body) { // Check required fields rq.expect(repo).to.include.keys(["id", "name", "private", "owner"]) rq.expect(repo.owner).to.include.keys(["login", "id"]) // Match regex rq.expect(repo.name).to.match(/^\.?[a-z0-9-_\.]+$/i) } })
This test works, but it's very verbose. If you have a more complicated JSON schema, you'll have to spend a lot of time writing out each property one by one. This is also unmaintainable. If the schema changes, changing it in the test will feel like finding a needle in a haystack. You can use .to.have.jsonSchema
instead, which lets you pass a JSON schema.
For example, you can write the above test:
rq.test("Match JSON schema", () => { rq.response.to.have.jsonSchema({ type: "array", items: { type: "object", required: ["id", "name", "private", "owner"], properties: { id: { type: "number" }, name: { type: "string", pattern: "^\\.?[a-zA-Z0-9-_\\.]+$" }, private: { type: "boolean" }, owner: { type: "object", required: ["login", "id"], properties: { login: { type: "string" }, id: { type: "number" } } } } } }) })
This is much easier to write, understand, and maintain. Of course, let's make sure the tests work.
Testing Complete Workflows
Often, your API calls are not as simple as calling one single endpoint. You might need to call one endpoint, process the response, and use the data from the response to call another endpoint. With Requestly, you can use Environment Variables to store arbitrary data and use them in subsequent requests. We have already used one environment variable in the Login endpoint. Let's now take a look at a more complete "workflow" example.
Let's assume that we want to test the pipeline of creating, fetching, and deleting issues. We want to create an issue, fetch the same issue, and then delete it.
In the Create issue endpoint, we're making a request to {{base_url}}/repos/{{owner}}/{{repo}}/issues
endpoint. The {{...}}
denotes an environment variable that Requestly will substitute. We're creating an issue with the following body:
{ "title": "Test issue from Requestly", "body": "This is a test issue created via API" }
Let's write a test to ensure the issue is created successfully:
rq.test("Issue was created successfully", () => { rq.response.to.be.created rq.response.to.have.jsonBody("title", "Test issue from Requestly") var body = JSON.parse(rq.response.body) rq.environment.set("issue_id", body.number) })
Here, we're checking that the title of the issue matches what we sent in the body. The last line is where we set the issue_id
environment variable, which we'll use in our subsequent request.
Open the Fetch issue request. You'll notice we're using the issue_id
in the request URL: {{base_url}}/repos/{{owner}}/{{repo}}/issues/{{issue_id}}
.
Let's write a test to verify the correct issue is fetched. We'll verify that it has title
and body
fields, and matches what we created:
rq.test("Issues has correct schema", () => { rq.response.to.have.jsonSchema({ type: "object", required: ["title", "body"], properties: { "title": { type: "string" }, "body": { type: "string" } } }) }) rq.test("Fetches correct issue", () => { rq.response.to.have.jsonBody("title", "Test issue from Requestly") rq.response.to.have.jsonBody("body", "This is a test issue created via API") })
Finally, let's close the issue to complete the workflow. In the Close issue request, you'll see we're again using the issue_no
variable. This time, let's make sure state
is closed
in the response:
rq.test("Issue is closed", () => { rq.response.to.have.jsonBody("state", "closed") })
You can use this technique of chaining requests in many real-life scenarios. For example, if your API uses username and password to log in and returns a JWT, you can store it in an environment variable and use it in subsequent tests.
Error Handling and Debugging
So far, we've been dealing with tests that work correctly. But in practice, tests may fail. And it's not always easy to debug. Suppose the login test is suddenly failing in CI. Is it because someone pushed buggy code, or is it due to something in the test itself, such as an expired token? Requestly offers debugging features to help you understand why your tests are failing.
When a test fails, Requestly prints an error message next to the test. You can try this out by sending the Invalid token request. This request attempts to log in with an invalid token, but expects the result to be 'ok'.
You can also use console.log
to log values. You can use rq.request
and rq.response
objects to access the request and response values to help you debug the actual cause of a failing test. For example, let's log the request headers and response status code:
rq.test("Reuest is successful", () => { console.log(JSON.stringify(rq.request.headers)) console.log(rq.response.status) rq.response.to.be.ok })
When you press Ctrl+Alt+I
, the Devtools console will open. If you send the request, the logs will show up.
These logs can help you debug your test.
Mocking & Intercepting for API Testing
We’ve seen how to handle real errors like bad tokens or missing endpoints. But what about failures that don’t happen on their own? For example, a flaky third-party API or an edge case response you can’t easily trigger in production. That’s where mocking comes in.
Real apps often depend on third-party APIs, such as payment processors, weather data, and analytics. Testing these integrations is tricky because:
- You can’t control when the third-party API fails.
- You can’t control the data it sends back.
- Sometimes, every test call costs real money.
This is where API mocking helps. With Requestly, you can intercept an outgoing request and return a fake response. That way you can simulate failures, edge cases, or alternate data without touching the real API.
Our demo repo has a /httpbin
endpoint that calls http://httpbin.org/get
. Normally, it just passes through the response. But what if HttpBin goes down? We want to see if our app handles errors gracefully.
To mock an API call, you need to use Requestly's HTTP interceptor, which turns Requestly into a proxy server to intercept HTTP calls from your browsers (Chrome, Firefox, etc.), terminal app, or even the whole system. Then you can use HTTP rules to modify the request, response, headers, introduce delays, redirect, and much more.
As of writing this article, this feature is not available in a local workspace. So, you'll need either a team workspace or switch to the private workspace. In any case, you'll need an account. Click on the icon in the top-right corner, and sign up or sign in.
Now in the workspace dropdown menu, you'll see that the Private workspace option is enabled. Choose this option to switch to the private workspace.
Go to the Network tab, click on Connect apps, switch to the Terminal processes tab, click on Setup instructions, and copy the command.
Paste the command in the terminal and run it. On successful execution, you should see the message Requestly interception enabled. Now start the server with npm start
. Now, any request made from the API server will pass through Requestly.
Note: Running the command provided by Requestly sets up a few environment variables, one of them being
https_proxy
, which sets up Requestly as a proxy server. The demo app uses Axios to make requests to HttpBin. Axios automatically picks up thehttps_proxy
environment variable, so in this case, we don't have to change any code. However, suppose you use a different library (such as node-fetch) that doesn't automatically pick up thehttps_proxy
environment variable. In that case, you'll need to modify the code to use the proxy server manually.
In another terminal, invoke the /httpbin
endpoint:
curl localhost:3000/httpbin
In Requestly, you should see the HttpBin request show up:
Note: As of writing the article, requests made with Axios show up with the wrong URL due to a bug.
Right-click on it and select Modify response body.
This will open a window where you can define an HTTP rule that will modify the response body of the intercepted request.
First, give the rule a name, for example: "HttpBin error". In the If request field, select URL and Contains and write httpbin.org
in the input field. Select 500
from the status codes dropdown, enter an empty body, and check Serve this response body without making a call to the server. Then, save the rule.
Now, anytime our API server makes a request to HttpBin, Requestly will return a 500 response without actually hitting HttpBin.
Let's now write the test. Go to APIs tab, and create a new HTTP request. Provide a name such as Get third party error, and put http://localhost:3000
in the URL field, and write the test:
rq.test("Should return error", () => { rq.response.to.have.status(500) })
Now I can test whether my app correctly handles failures or not without having to wait for the third-party API to fail!
With these tests in place - from happy paths to deliberate failures - we've covered the core scenarios of real-world API testing. Now it's your turn to use Requestly for testing your APIs.
Conclusion
Effective API testing goes beyond checking if an endpoint returns data - it ensures reliability, catches edge cases, and streamlines development. By going local-first with Requestly, you can quickly test and mock APIs without juggling cloud accounts or heavy setups. Start small with a few tests, then layer on more complexity as your needs grow. The best way to see the value is to try it in your own workflow and feel how much smoother debugging and iteration can become.
Top comments (0)