DX SDK and code evolution. This article looks at SDKs covering… | by Pavel Durov | September 2023


Image of the author

This article takes a look at SDKs (Software Development Kits), covering their development, maintenance and crucial aspects of DX (Developer Experience). We’ll explore the fundamentals of DX with TypeScript examples and examine the evolution of the code.

The SDK integrates with external systems such as remote APIs (Application Programming Interface), local ABI (Application Binary Interface), devices or hardware platforms. It is a collection of software components grouped into a single package.

This package contains everything needed to effectively use the underlying system for which the SDK provides functionality.

But it’s not enough to have a working SDK. If we want the SDK to be adopted and survive the test of time, it must also provide a good user experience. We call this experience DX since developers are the primary users of SDKs.

Why create an SDK?

SDKs provide a streamlined approach to building applications for specific targets. They function as specialized toolboxes.

One of their main advantages is the simplification of the onboarding process. This simplification is achieved by often hiding the complexities of internal implementation and providing an intuitive interface.

Additionally, the SDK is a reusable component. It enables seamless integration across multiple projects, reducing code duplication and facilitating support and maintenance.


An API (Application Programming Interface) exposes the internal functionality of a system without exposing its internals in a language-independent manner.

Distinctively, SDKs are tailored to specific programming languages, while APIs maintain a higher level of abstraction. This distinction makes SDKs more user-friendly and easily adoptable due to their simple integration and development experience.

Usually, SDKs use certain APIs in the background while enhancing them with additional features, extensive documentation, and practical examples.

DX (Developer Experience) describes the interactions and experience a software developer has when working with a tool or 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.

This can be subjective, but it’s hard to deny great DX.

When evaluating DX, we need to consider several factors.

DX — explicit functionality

Although this principle may seem elementary, it is nevertheless essential and sometimes neglected.

A tool must do precisely what it claims to do. Surprisingly, many tools are prone to doing things that a developer could not reasonably predict.

Consider this scenario: You have integrated an SDK into your project to use a remote Restful API. Yet when using it, it unexpectedly generates large files on your disk due to an unexpected optimization process that was never mentioned.

DX — complete documentation

Documentation doesn’t need to be wordy, but it should be precise. Developing clear documentation is one of the most difficult parts of software engineering.

Documentation must remain up to date, striking a balance between brevity and completeness.

DX — intuitive and easy to use

This should be intuitive. A developer should look at the code and immediately understand how to use it without the need for extensive documentation exploration.

When adapted to a specific programming language, it must faithfully adhere to the conventions of the language and avoid unnecessary deviations. The appearance of the code should be familiar and accessible.

End-to-end use of the tool should also be simple. This includes installation, configuration and actual usage.

DX — adaptability

It must be designed to be flexible and adaptable. This includes modularity, configuration options and versioning.


To achieve good DX, the software must be designed with compatibility in mind.
The worst DX is when you update your SDK version, and suddenly you have to repair all the places used in the project.

Discussing different types of compatibility, such as forward, forward, and full compatibility, is beyond the scope of this article. However, it is crucial to define SDK compatibility.

DX – quick starts and examples

The compact, working examples that give a comprehensive overview of the tool’s capabilities are invaluable. They trigger those “AHA” moments where everything effortlessly falls into place when using the sample provided.

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 saw it, I was blown away.

Let’s talk specifically about the TypeScript SDK.

To deliver good DX, we must first understand the customer. We have to ask ourselves: what do TypeScript engineers expect from the SDK?

To name just a few of these expectations:

  • Easy to use
  • Promises and Async/Await — asynchronous functionality by default.
  • Package manager support – installing with one of the goto package managers like npm
  • Working code examples: copy, paste, execute.
  • Type Definitions — TypeScript is a statically typed language. Types are treated as a basic component.
  • Type safety — type safety must be enforced in all interfaces.
  • Module support — compatibility with modern module systems such as CommonJS and ES6 modules

In the following examples, we will attempt to address most of these points, focusing on the evolution of the code.

Example: publishing the 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 using the TypeScript SDK:

import Posts from 'posts';
const posts = new Posts();
const post = await posts.createPost('title', 'content');

This is how we expect our SDK to be used by our users.

We’ll call it V1.

Code evolution and optional parameters

Let’s discuss optional parameters and how they affect the evolution of the code.

Consider our SDK createPost function:

function createPost(title: string, content: string): Promise<Post> { 
/* ... */

Let’s say we want multiple ways to create Posts in our system.

We don’t want to disrupt the current use of this feature. We want to introduce new features while keeping the SDK compatible with previous versions (i.e. backwards compatibility).

The obvious tool of choice for this job is, you guessed it, optional settings.

Here’s how to do it:

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 turning into something weird.

Intuitively, I would expect that title be the first argument of the function, followed by the subtitle and then the content. But we cannot change the order at will. We will break V1 compatibility. If we did this it would mean that for use in V1 all content would suddenly be set as 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 like:

createPost("My Title", "My Content", undefined, new Date());

It’s not great at all.

Looking at the code, it’s difficult to understand what is defined as undefined – it’s NOT intuitive.

So, what is 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 like:

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()

It’s much more readable and intuitive!

It has no specific order of settings and, more importantly, no radical changes.

It’s easier to scale functionality based on types rather than order of function parameters.

When we design software, we need to think about how it will evolve and allow it to be extended in a compatible way.

We explored the area of ​​SDKs and their applications and delved deeper into the importance of DX and what good DX looks like.

We looked at various practical examples of TypeScript and a brief discussion on the impact of optional parameters and alternative ways to evolve code over time without introducing radical changes.

This article was written out of my own desire to understand and organize my thoughts since it was about sharing knowledge.
I hope this was helpful.


Source link

Related Articles

Back to top button