How To Log API Calls
Greetings, friends! Have you ever wondered how frontend logging libraries implemented by companies such as LogRocket and New Relic capture information about your API calls? It's actually quite simple! However, it does require a good understanding about JavaScript's prototypal nature and the "this" keyword. What makes JavaScript so captivating is its flexibility, which allows us developers to create so many wonderful libraries with all kinds of interesting design patterns, but I digress! Let's see how to log API calls!
Overriding the XMLHttpRequest prototype
JavaScript has multiple ways of reaching out to a server. One way is by using XMLHttpRequest objects. Many common libraries you may use to make API calls, such as axios, use XMLHttpRequest objects internally. This means that if you override methods on the XMLHttpRequest prototype, then it will affect API calls you make using axios.
The XMLHttpRequest prototype contains multiple methods for setting up an interaction with a server. In this example, we will be intercepting calls made to XMLHttpRequest.prototype.open and simply log the method of the API call and the URL we reach out to. We will make API calls to JSON Placeholder, an awesome fake online REST API. It's a great way to test candidates on their knowledge of asynchronous programming during interviews 🤫.
console.log('------OVERRIDING XMLHTTPREQUEST.OPEN METHOD-----')
const originalOpen = XMLHttpRequest.prototype.open
// Note: You should not use an arrow function here, since you need the "this" value scoped to the XMLHttpRequest object
XMLHttpRequest.prototype.open = function () {
console.log(...arguments)
originalOpen.call(this, ...arguments)
}
function getUser(num) {
const xhr = new XMLHttpRequest()
xhr.open('GET', `https://jsonplaceholder.typicode.com/todos/${num}`)
xhr.responseType = 'json'
xhr.onload = () => {
console.log(xhr.response)
}
xhr.send()
}
console.group('API Calls')
getUser(1)
getUser(2)
getUser(3)
console.groupEnd()
If you run this code in the developer console, then you should see something like this:
Let's analyze the example and discuss what's happening. The XMLHttpRequest.prototype.open method is responsible for initializing or re-initializing a new request. It accepts five parameters according to MDN. We will first save a reference to the original method.
const originalOpen = XMLHttpRequest.prototype.open
Next, we will override this method. The arguments
object is a special object in JavaScript that contains an array of all the arguments we pass to a function. We will use the spread syntax to spread out the arguments
array into individual paramters that will be passed into console.log()
. Then, we will call the original XMLHttpRequest.prototype.open method using the XMLHttpRequest as the "this" value. It's important that we don't use an arrow function because we would get a ReferenceError
saying arguments
is undefined otherwise.
XMLHttpRequest.prototype.open = function () {
console.log(...arguments)
originalOpen.call(this, ...arguments)
}
Once we override the XMLHttpRequest.prototype.open method, we can now use it! We'll make a function that will get a user from the JSON Placeholder API.
function getUser(num) {
const xhr = new XMLHttpRequest()
xhr.open('GET', `https://jsonplaceholder.typicode.com/todos/${num}`)
xhr.responseType = 'json'
xhr.onload = () => {
console.log(xhr.response)
}
xhr.send()
}
For some extra flair, we can group the logs together and label the group. This will make it easy to detect the API calls we logged using the overridden XMLHttpRequest.prototype.open method.
console.group('API Calls')
getUser(1)
getUser(2)
getUser(3)
console.groupEnd()
Overriding the fetch method
The fetch API has become a very common method for reaching out to a server because it requires very little setup compared to XMLHttpRequest objects. Additionally, it returns a promise, which fits in well with libraries that use promises and makes it easy to chain API calls together.
Logging API calls using fetch is very similar to the approach above for overriding the XMLHttpRequest.prototype.open method. Instead of overriding a prototype, we simply just save a reference to the original fetch method and call it later.
console.log('------OVERRIDING FETCH METHOD-----')
const originalFetch = fetch
// Note: You should not use an arrow function here, since you need the "this" value scoped to fetch
fetch = function () {
console.log(...arguments)
return originalFetch.call(this, ...arguments)
}
function getUser(num) {
fetch(`https://jsonplaceholder.typicode.com/todos/${num}`)
.then(response => response.json())
.then(data => console.log(data))
}
console.group('API Calls')
getUser(1)
getUser(2)
getUser(3)
console.groupEnd()
Conclusion
That's it! You now have the tools to override tons of native JavaScript methods! Using the approach you have learned today, you can intercept not only API calls, but almost any function call in JavaScript! As a precaution though, I would only override them if it's absolutely necessary. The Proxy object allows you to intercept and redefine a lot of operations in native JavaScript. However, there are times where you need to override the original method such as in this case where we want to perhaps create our own logging library. Until next time, happy coding! :)