Abstract
This article delves into SDKs (Software Development Kit), covering their development, maintenance, and the crucial aspects of DX (Developer Experience). We’ll explore DX core principles with TypeScript examples and examine code evolution.
Introduction
SDK integrates with external systems such as remote APIs (Application Programming Interface), local ABIs (Application Binary Interface), devices, or hardware platforms. It is a collection of software components bundled as one package.
This package contains everything necessary to effectively use the underlying system for which the SDK provides functionality.
But it’s not enough to have a functional SDK. If we want the SDK to be adopted and survive the test of time, it must also have a good user experience. We call this experience DX since developers are the main users of SDKs.
Why build SDK?
SDKs offer a streamlined approach to crafting applications for specific targets. They function as specialised toolkits.
One of their key benefits lies in simplifying the integration process. This simplification is achieved by often hiding the complexities of internal implementation and providing an intuitive interface.
Additionally, SDK is a reusable component. It allows seamless integration into multiple projects, reducing code duplication, and facilitating support and maintenance.
SDK vs API
An API (Application Programming Interface) exposes the internal functionality of a system without exposing its internals in a language-agnostic fashion.
Distinctively, SDKs are tailored to specific programming languages, while APIs maintain a higher level of abstraction. This distinction makes SDKs more user-friendly and readily adoptable due to their straightforward integration and developer experience.
Usually, SDKs use some API behind the scenes while enhancing it with additional functionality, comprehensive documentation, and practical examples.
DX
DX (Developer Experience) describes a software developer’s interactions and experience when working with a tool or a piece of code.
If you are familiar with the term UX (User Experience), then you can think of DX in the same terms where the user is the developer.
It might be subjective, but great DX is hard to deny.
When evaluating DX, we should consider multiple factors.
DX — explicit functionality
While this principle may appear elementary, it’s essential and sometimes overlooked.
A tool should precisely do what it claims to do. Surprisingly, numerous tools are inclined to do things a developer would not reasonably anticipate.
Consider this scenario: You’ve integrated an SDK into your project to use some remote Restful API. Yet, upon its use, it unexpectedly generates hefty files on your disk due to an unexpected optimization process that was never mentioned.
DX — comprehensive documentation
Documentation does not need to be verbose, but it should be precise. Crafting clear documentation is one of the most challenging parts of software engineering.
Documentation must remain up-to-date, striking a balance between brevity and comprehensiveness.
DX — intuitive and easy to use
It should be intuitive. A developer should look at the code and immediately understand how to work with it without the need for extensive documentation exploration.
When tailored to a specific programming language, it should faithfully stick to the language’s conventions and avoid unnecessary deviations. The code’s appearance should be familiar and approachable.
The end-to-end use of the tool should be easy as well. That includes installation, configuration, and actual use.
DX — adaptability
It should be designed to be flexible and adaptable. That includes modularity, configuration options, and version management.
DX — compatibility
To achieve good DX, software needs to be designed with compatibility in mind.
The worst DX is when you upgrade your SDK version, and suddenly, you must fix all the places used in the project.
Examining different compatibility types, such as backward, forward, and full compatibility, is beyond the scope of this article. However, it is crucial to define the compatibility of the SDK.
DX — quickstarts and samples
Compact, functional examples that provide a comprehensive glimpse of the tool’s capabilities are priceless. They trigger those “AHA” moments when everything effortlessly falls into place upon using the provided sample.
One of the best quickstarts I’ve seen is node.js express:
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
In just 11 lines, we can get a server up and running. The first time I’ve seen it, I was blown away.
Node.js and TypeScript SDK
Let’s talk about TypeScript SDK specifically.
To deliver a good DX, we need to understand the client first. We need to ask — What do TypeScript engineers expect from the SDK?
To name a few of these expectations:
- Easy-to-use
- Promises and Async/Await — async functionality by default.
- Package manager support — installation with one of the goto package managers like npm
- Functional code examples — copy, paste, execute.
- Type definitions — TypeScript is a statically typed language. Types are treated as a basic component.
- Type safety — type safety should be enforced throughout the interfaces.
- Modules support — compatibility with modern module systems like CommonJS and ES6 modules
In the following examples, we will try to address most of these points, focusing on the code evolution.
Example: posts API SDK
Let’s say we have the following Restful API:
POST /posts - Creates new post
PUT /posts/{id}/like - Like a post
Let’s translate these endpoints to TypeScript SDK usage:
import Posts from 'posts';
const posts = new Posts();
const post = await posts.createPost('title', 'content');
await posts.like(post.id);
That’s how we expect our SDK to be used by our users.
We’re going to call it V1.
Code evolution and optional parameters
Let’s discuss optional parameters and how they affect code evolution.
Consider our SDK createPost function:
function createPost(title: string, content: string): Promise<Post> {
/* ... */
}
Let’s say we want multiple ways of creating Posts in our system.
We don’t want to break the current usage of this function. We want to introduce new functionality while keeping the SDK compatible with previous versions (aka backward compatibility).
The obvious tool of choice for this job is, you guessed it right — optional parameters.
Here’s how we can do that:
function createPost(title: string, content: string, subtitle?: string): Promise<Post> {
/* ... */
}
Now, we can use it in both ways:
import Posts from 'posts';
const posts = new Posts();
await createPost("My Title", "My Content"); // V1
await createPost("My Title", "My Content", "My Subtitle"); // V2
And it’s already morphing into something weird.
Intuitively, I would expect the title to be the first function argument, followed by the subtitle and then the content. But we can’t just change the order at will. We will be breaking V1 compatibility. If we did, it would mean that for V1 usage, all the content would suddenly be set as a subtitle, which is unacceptable.
And what will happen when we add another optional parameter to our function?
function createPost(
title: string,
content: string,
subtitle?: string,
date?: Date): Promise<Post>{
/* ... */
}
Now, this function can be used as:
createPost("My Title", "My Content");
createPost("My Title", "My Content", "My Subtitle", new Date());
But also as:
createPost("My Title", "My Content", undefined, new Date());
That’s not great at all.
Looking at the code, it’s hard to understand what is set as undefined — it’s NOT intuitive.
So, what would be better to use in this case?
We can use objects!
interface Params {
title: string;
subtitle?: string;
content?: string;
date?: Date;
}
function createPost(params: Params) : Promise<number> { /* ... */ }
Now, we can use our function as:
await createPost({
title: "My Title", ,
content: "My Content",
});
await createPost({
title: "My Title",
subtitle: "My Subtitle",
content: "My Content",
});
await createPost({
title: "My Title",
subtitle: "My Subtitle",
content: "My Content",
date: new Date()
});
That’s way more readable and intuitive!
It has no specific parameter ordering and, most importantly, no breaking changes.
Evolving the functionality based on types rather than function parameter order is easier.
When we design software, we need to consider how it will evolve and allow it to be extended in a compatible way.
Summary
We explored the realm of SDKs and their applications and delved into the significance of DX and what a good DX looks and feels like.
We examined various practical TypeScript examples and a short discussion of the impact of optional parameters and alternative ways to evolve code over time without introducing breaking changes.
This article was written for my own sake of understanding and the organisation of my thoughts as it was about knowledge sharing.
I hope it was useful.
SDK DX and Code Evolution was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.