Skip to main content Accessibility Feedback

Data portability: API getter and setter methods

When I advise clients on how to work with APIs, I recommend never calling API’s directly.

Instead, I suggest creating functions whose sole purpose is to get and set API data. It allows you to change the API endpoint and even how your API returns data without having to rewrite significant portions of your code base.

Let’s dig in!

An example

Let’s imagine you have a todo app.

Users can view a list of their todo items, add new items, and mark items as complete. Each of these actions makes a call to an API that gets, sets, or modifies data.

Let’s say the API endpoint is /todos.

A GET request returns all of the data, a POST request creates a new todo, and a PUT request updates an existing todo item.

// Data returns from GET
let data = [{
	id: 1234
	todo: 'Play D&D',
	completed: false
},
{
	id: 5678
	todo: 'Buy milk',
	complete: true
}];

// Data format for POST and PUT methods
let update = {
	id: 1234
	todo: 'Play D&D',
	completed: true
};

Direct-use endpoints

I sometimes see API endpoints called directly in the code that uses their data.

async function renderTodos () {

	// Get the API data
	let request = await fetch('/todos');
	let todos = await request.json();

	// Render the data
	let app = document.querySelector('#app');
	app.innerHTML = 
		`<ul>
			${todos.map((todo) => {
				return `<li>${todo}</li>`;
			}).join('')}
		</ul>`;

}

This works just fine… until it doesn’t.

APIs change

There’s a good chance your app calls the API in various places for various tasks. When the API changes, that means a lot of rewrites.

Maybe the endpoint changes from /todos to /api/v2/todos. A simple enough change, but potentially scattered across different files.

Maybe the way authentication is handled changes. Now you need to update your fetch() request everywhere it’s used.

Sometimes, the data that’s returned changes.

// The original GET response
let data = [{
	id: 1234
	todo: 'Play D&D',
	completed: false
},
{
	id: 5678
	todo: 'Buy milk',
	complete: true
}];

// The new GET response
let newData = {
	1234: {
		todo: 'Play D&D',
		completed: false
	},
	5678: {
		todo: 'Buy milk',
		complete: true
	}
};

Now, anywhere your API is called, you need to not just update the API call, but refactor how you work with it.

But there’s an easier way.

Getter and setter methods

I recommend my clients use dedicated methods for calling APIs and processing the data that’s returned.

// Get the API data
async function getTodos () {
	let request = await fetch('/todos');
	let todos = await request.json();
	return todos;
}

// Render todo data
async function renderTodos () {
	let todos = await getTodos();
	let app = document.querySelector('#app');
	app.innerHTML = 
		`<ul>
			${todos.map((todo) => {
				return `<li>${todo}</li>`;
			}).join('')}
		</ul>`;
}

If the API changes, you now have a single file where you can make your changes.

And for more severe changes, like a big change in how the data is returned, you can transform it before returning it and avoid ever having to update the functions that use it at all.

// Get the API data
async function getTodos () {

	// Get API data
	let request = await fetch('/todos');
	let todos = await request.json();

	// Transform the data into the old format
	let transformed = Object.entries(todos).map(([id, item]) => {
		let {todo, completed} = item;
		return {id, todo, completed};
	});

	// Return the transformed data
	return transformed;

}

With this approach, renderTodos() and any other function that uses the output of getTodos() doesn’t need to change.

The specifics of the API become an implementation detail you abstract away.

If you or your team need help structuring code for long term maintenance and scalability, get in touch. I’d love to help!