AI for Software Development: Best Practices and Tools
Douglas da Silva | Mar 24, 2025
In today’s diverse frontend ecosystem, building tools that cater to a single framework can limit their reach and usability. Not only this, you may also want to target the whole frontend ecosystem, without limitations related to libs and frameworks.
Framework-agnostic SDKs offer a solution by ensuring compatibility across multiple frameworks while maintaining performance and simplicity.
This blog post aims to guide you through the process of creating a framework-agnostic web SDK, from initial setup to deployment. Whether you’re a seasoned developer or new to SDK design, you’ll gain insights and practical tips to make your SDK accessible to developers everywhere.
A framework-agnostic SDK is designed to work seamlessly across multiple frameworks, such as React, Vue, Angular, or even vanilla JavaScript.
The goal is to maximize usability and minimize dependency on specific tools or libraries, ensuring that developers working in various ecosystems can integrate your SDK without friction.
This approach not only broadens your SDK’s audience but also simplifies maintenance and reduces versioning headaches.
Key aspects of a framework-agnostic SDK:
Selecting the right stack is critical when building a framework-agnostic SDK. Here’s a breakdown:
A well-designed API is the cornerstone of a successful SDK. So focus on:
Modularity
import { FullSDK } from 'sdk';, enable import { SpecificFeature } from 'sdk';.
Backward Compatibility
Documentation
Let’s give a quick example of what a framework-agnostic SDK would look like.
For this example, we will create an SDK for managing custom keyboard shortcuts across a web application, allowing developers to easily bind and unbind shortcuts to specific actions.
Ctrl + S
) to trigger specific functions.A good way to start is to define how you will use the SDK. Given the features above, we should use the SDK as follows:
import { initialize } from './keyboardShortcutSDK';
initialize();
Code language: JavaScript (javascript)
<code>import { keyboardShortcutSDK } from './keyboardShortcutSDK';
keyboardShortcutSDK().register('Ctrl+S', () => {
console.log('Save triggered!');
alert('Document saved!');
});</code>
Code language: JavaScript (javascript)
import { keyboardShortcutSDK } from './keyboardShortcutSDK';
const modalIsOpen = () => document.querySelector('#myModal').classList.contains('open');
keyboardShortcutSDK.register('Escape', () => {
console.log('Modal closed!');
document.querySelector('#myModal').classList.remove('open');
}, { condition: modalIsOpen });
Code language: JavaScript (javascript)
import { keyboardShortcutSDK } from './keyboardShortcutSDK';
keyboardShortcutSDK.unregister('Ctrl+S'); // Remove the save shortcut
Code language: JavaScript (javascript)
5. Show a tooltip when registering a shortcut
import { keyboardShortcutSDK } from './keyboardShortcutSDK';
keyboardShortcutSDK.register('Ctrl+S', () => {
console.log('Save triggered!');
alert('Document saved!');
}, { feedback: { enabled: true, message: "Press 'Ctrl+S' to save." } });
Code language: JavaScript (javascript)
For example purposes, the SDK will be whole exported in keyboardShortcutSDK
, but keep in mind modularity when creating your SDK, as we saw in the core considerations session.
Let’s initialize the Vite project:
npm create vite@latest
Code language: JavaScript (javascript)
Give it a name when prompted. Select ‘Vanilla’ when prompted for a framework (which means no specific framework will be used), and then choose TypeScript as the variant.
Follow the instructions given by Vite’s cli: go to the folder created with the project name and run `npm install` (or use any package manager you want).
After that, you can run npm run dev
, and open http://localhost:5173 in your browser to see the following:
Fun fact: Do you know why Vite uses the port 5173? If you try to read it as letters, you will read SITE!
If you open your project in the editor/IDE of your choice, you will see the following structure:
The first thing to do is to tweak the configurations.
Open tsconfig.json
file, and add the following to the compilerOptions
object:
"declaration": true,
"declarationMap": true,
"sourceMap": true
Code language: JavaScript (javascript)
Those configs will export the declaration files for the lib when compiling, and also will export the source maps, which help with debugging steps.
Also, add the app
folder to the include
array. We will be creating this folder in the next steps. The configuration file should look like the following:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": [
"src",
"app"
]
}
Code language: JavaScript (javascript)
After that, create a new file called `tsconfig.build.json` with the following content:
{
"extends": "./tsconfig.json",
"include": [
"src"
]
}
Code language: JavaScript (javascript)
When building, this will override the include
list to include only the SDK source code.
Now, let’s create a file called vite.config.ts
. This is the file responsible for the configurations of the Vite project. It’s here where we will configure Vite to the lib mode.
Before creating it, let’s add the development dependencies we will need:
@types/node
: types for node packagesvite-plugin-dts
: the Vite plugin responsible for generating our *.d.ts files
:npm i -D @types/node@latest vite-plugin-dts
Code language: JavaScript (javascript)
Then, create the vite.config.ts
file with the following contents:
import { resolve } from "path"
import { defineConfig } from "vite"
import dts from "vite-plugin-dts"
export default defineConfig({
plugins: [dts({ include: ["src"] })],
build: {
copyPublicDir: false,
lib: {
entry: resolve(__dirname, "src/index.ts"),
name: "keyboard-shortcut-sdk",
formats: ["es", "cjs", "umd"],
fileName: (ext) => `index.${ext}.js`,
},
sourcemap: true,
},
})
Code language: JavaScript (javascript)
Till now, you have had a file structure like this:
We need to move what we don’t want in the SDK to the app folder. Let’s then create this folder and move the index.html
file there.
Also, let’s remove the code we will not be using:
counter.ts
, style.css
, and typescript.svg
from the src
folderRename main.ts
to index.ts
, and update the content with the following:
export function helloAnything(thing: string): string {
return `Hello, ${thing}!`
}
Code language: JavaScript (javascript)
Now you can observe that if you try to start the server again, you’ll see nothing in the browser. But if you try to access the route /app/index.html
, the index.html
file will load. Let’s fix that. In the vite.config.ts file
, add the following to the plugins
list:
{
name: "deep-index",
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === "/") {
req.url = "/app/index.html"
}
next()
})
},
},
Code language: JavaScript (javascript)
Also, inside the app
folder, create a new file called main.ts
, and add the following content:
import { helloAnything } from "../src"
console.log(helloAnything("world"))
Code language: JavaScript (javascript)
And update the script importing in the index.html
accordingly:
<script type="module" src="/app/main.ts"></script> <!-- it was /src/main.ts before updating -->
Code language: JavaScript (javascript)
Now, you are able to open the webpage. It will be a blank screen. If you open the console, you should see Hello, world!
printed.
And this is the file structure now:
Now that we have the project structure prepared, let’s develop the features of the SDK.
Let’s create the initializeSdk
function. It will serve only to create the SDK instance to be used later. But you can use this function to save an access token, for example, or save some initial configs that your solution could be using. Let’s create the domain
folder, where we will be adding all our business logic.
And inside that, let’s add an sdk
folder, focused on the SDK main class. There, add the index.ts
and put the following code:
export class KeyboardShortcutSDK {
private static instance: KeyboardShortcutSDK
static getInstance() {
if (!KeyboardShortcutSDK.instance) {
throw new Error("KeyboardShortcutSDK is not initialized")
}
return KeyboardShortcutSDK.instance
}
static initialize() {
KeyboardShortcutSDK.instance = new KeyboardShortcutSDK()
return KeyboardShortcutSDK.instance
}
}
Code language: JavaScript (javascript)
Now, update the src/index.ts
file content to the following:
import { KeyboardShortcutSDK } from "./domain/sdk"
export function initializeSdk() {
return KeyboardShortcutSDK.initialize()
}
export function keyboardShortcutSDK() {
return KeyboardShortcutSDK.getInstance()
}
Code language: JavaScript (javascript)
You will see that the IDE will start complaining about app/main.ts
. Let’s update it also to call our initialize function:
import { initializeSdk } from "../src"
initializeSdk()
Code language: JavaScript (javascript)
Now that we already have the SDK class, let’s add the function to register the shortcuts.
Add this type somewhere (I added it before the SDK class definition):
type KeyboardShortcut = {
key: string
ctrlKey: boolean
shiftKey: boolean
altKey: boolean
metaKey: boolean
callback: () => void
options?: {
condition?: () => boolean
feedback?: string
}
id: string
}
Code language: JavaScript (javascript)
Add the following code to the SDK class:
private shortcuts: KeyboardShortcut[] = []
private initializedShortcutsListener = false
private parseShortcut(shortcut: string) {
const shortcutPieces = shortcut.toLowerCase().split("+")
if (shortcutPieces.length > 2) {
throw new Error(
'Invalid shortcut. Use at most a modifier and a key. Example: "ctrl+s"'
)
}
const [modifierOrKey, key] = shortcutPieces
const ctrlKey = modifierOrKey === "ctrl"
const shiftKey = modifierOrKey === "shift"
const altKey = modifierOrKey === "alt"
const metaKey = modifierOrKey === "meta"
return {
key: key || modifierOrKey,
ctrlKey,
shiftKey,
altKey,
metaKey,
id: shortcut,
}
}
private initializeShortcutsListener() {
const onKeyPressed = (event: KeyboardEvent) => {
const shortcut = this.shortcuts.find((shortcut) => {
return (
shortcut.key === event.key &&
shortcut.ctrlKey === event.ctrlKey &&
shortcut.shiftKey === event.shiftKey &&
shortcut.altKey === event.altKey &&
shortcut.metaKey === event.metaKey
)
})
const shouldRun = shortcut?.options?.condition?.() ?? true
if (shortcut && shouldRun) {
event.preventDefault()
shortcut.callback()
}
}
document.addEventListener("keydown", onKeyPressed)
}
register(
shortcut: string,
callback: () => void,
options?: {
condition?: () => boolean
feedback?: string
}
) {
this.shortcuts.push({
...this.parseShortcut(shortcut),
callback,
options,
})
if (!this.initializedShortcutsListener) {
this.initializedShortcutsListener = true
this.initializeShortcutsListener()
}
}
Code language: JavaScript (javascript)
Explanation:
initializedShortcutsListener
is a flag to check if we have initialized the listener for shortcutsinitializeShortcutsListener()
will initialize the listener the first time a shortcut is registered.parseShortcut()
will parse the string passed in the register
method to check for modifiers. For simplicity, we will allow only shortcuts with a key or modifier+key.register()
is the main method where we will be registering shortcuts. It accepts, respectively:
Now we can register our first shortcut in the app/main.ts
file:
import { initializeSdk, keyboardShortcutSDK } from "../src"
initializeSdk()
keyboardShortcutSDK().register("ctrl+s", () => {
alert("Saved!")
})
Code language: JavaScript (javascript)
If you run the project and press “Ctrl+S“ on the keyboard, you should see the alert appear.
Let’s add a feedback code for when the shortcut is triggered.
First of all, we will be using TailwindCSS to demonstrate CSS injection. Following the instructions from TailwindCSS website:
<code>npm install -D tailwindcss @tailwindcss/vite</code>
Code language: JavaScript (javascript)
vite.config
:
plugins: [
tailwindcss(),
dts(...),
...
]
Code language: JavaScript (javascript)
<code>plugins: [ tailwindcss(), dts(...), ... ]</code>
Code language: JavaScript (javascript)
src
and add the import to tailwind:<code>@import "tailwindcss"</code>
Code language: JavaScript (javascript)
styles.css
to the src/index.ts file
:<code>import './styles.css'</code>
Code language: JavaScript (javascript)
Let’s then also add some content to our example app. Change the body
content of your index.html
to the following:
<body>
<div class="w-full h-screen bg-slate-800 text-white flex flex-col justify-center items-center">
<h1 class="text-2xl font-bold">Keyboard Shortcuts SDK</h1>
<h3 class="text-lg italic">Example app</h3>
<button id="register-save-shortcut" class="mt-10 px-4 py-2 rounded-md bg-amber-700">Register Save shortcut</button>
<p id="save-shortcut-content" class="hidden mt-10 text-gray-300">
Press
<span class="text-white font-bold">"Ctrl+S"</span>
for a shortcut demonstration
</p>
</div>
<script type="module" src="/app/main.ts"></script>
</body>
Code language: JavaScript (javascript)
It should now render the following page:
Let’s then add the vite-plugin-css-injected-by-js dependency
, which will be responsible for adding the generated styles of our lib injected in our JS output:
npm i -D vite-plugin-css-injected-by-js
Code language: JavaScript (javascript)
And update the plugins list in Vite:
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js"
Code language: JavaScript (javascript)
plugins: [
tailwindcss(),
cssInjectedByJsPlugin(),
dts(...),
...
]
Code language: JavaScript (javascript)
Now, if you run npm run build
the result should contain a function that adds the styles dynamically. Like the following:
But if you search for the class w-full
, which we are using in our index.html
file (which is an example page only), it will be in our generated file. And we don’t want this. We want to include only styles from our library in our output. To do that, because of the way Tailwind v4 is designed, we need to have multiple styles.css
files, one for development purposes with our example page, and another for building the SDK.
Let’s rename the styles.css
file to styles.dev.css
and keep the content as is. Also, let’s create another styles.css file now, with the following content:
@import "tailwindcss" source(none);
@source "../src";
Code language: CSS (css)
The source(none)
will change the content that tailwind expects to check to be nothing (by default, it will check for everything except for git-ignored files and some others – refer to Tailwind documentation). After this, we do a @source "../src";
which tells Tailwind that we want to include the src
folder.
Also, to load one file or another dynamically, let’s create the loadStyles.ts
file with the following:
function loadStyles() {
if (import.meta.env.DEV) {
import("./styles.dev.css")
} else {
import("./styles.css")
}
}
loadStyles()
Code language: JavaScript (javascript)
In the index.ts
file, change the import to ./styles.css
file to be:
import "./loadStyles"
Code language: JavaScript (javascript)
Now, if you run npm run build
, you will not see the w-full
class name present in the output files anymore. This way, we can add feedback for when a shortcut is registered and have only the desired styles in the output files.
Given this, let’s update our SDK to show a toast when the user passes a feedback string that shows up when the shortcut is registered.
Add the following method to our KeyboardShortcutSDK class:
private shortcutAddedFeedback(feedback: string) {
const toast = document.createElement("div")
toast.innerHTML = `
<div class="fixed inset-x-0 top-4 flex items-end justify-center px-4 py-6 pointer-events-none sm:items-start sm:px-6 sm:py-4 sm:justify-end sm:space-x-4 sm:top-6 sm:right-6">
<div class="z-50 flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow-sm dark:text-gray-400 dark:bg-gray-900" role="alert">
<div class="ms-3 text-sm font-normal">${feedback}</div>
</div>
</div>
`
document.body.appendChild(toast)
setTimeout(() => {
document.body.removeChild(toast)
}, 3000)
}
Code language: JavaScript (javascript)
Also, update the register
method to call it when the options.feedback
is defined. Add the following lines to the end of the register method:
if (options?.feedback) {
this.shortcutAddedFeedback(options.feedback)
}
Code language: JavaScript (javascript)
To make it in action, let’s also update our app/main.ts
to call the register only when the user clicks on the “Register Save shortcut“ button, and to include a feedback string:
import { initializeSdk, keyboardShortcutSDK } from "../src"
initializeSdk()
const buttonToRegisterShortcut = document.getElementById(
"register-save-shortcut"
)
const saveShortcutContent = document.getElementById("save-shortcut-content")
function registerShortcut() {
keyboardShortcutSDK().register(
"ctrl+s",
() => {
alert("Saved!")
},
{
feedback: 'Shortcut "ctrl+s" registered',
}
)
saveShortcutContent?.classList.remove("hidden")
buttonToRegisterShortcut?.classList.add("hidden")
}
buttonToRegisterShortcut?.addEventListener("click", registerShortcut)
Code language: JavaScript (javascript)
Now, if you click on the button, you will have the following:
Now, if you run npm run build
, it will only include the styles you have in the SDK source code.
To make an unregister
method, let’s add the following to the KeyboardShortcutSDK class:
unregister(shortcut: string) {
const index = this.shortcuts.findIndex((s) => s.id === shortcut)
if (index !== -1) {
this.shortcuts.splice(index, 1)
}
}
Code language: JavaScript (javascript)
NPM has the npm pack
command, which is responsible for generating a .tgz file with the package contents and everything deliverable in your package.
If you run this command and check the contents inside the .tgz file, you will see that everything was included in the package, including the development app and the src folder.
Since we are compiling everything, we want to deliver only the compiled files, so that we can keep the package as small as it can. To fix it, let’s add something to the package.json file.
Add the following to your package.json:
"main": "dist/index.es.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
Code language: JavaScript (javascript)
Now, if you run npm run build
, npm pack again
, and check the .tgz file content, you will have something like this:
You can also use the .npmignore
file to exclude some files from the final package. It works similarly to the .gitignore
file.
A good practice is to keep an updated README file for your SDK/Library. Let’s add one to ours:
# Keyboard Shortcut SDK
This project is a simple SDK for registering and handling keyboard shortcuts in a web application. It provides an easy-to-use interface for defining keyboard shortcuts and their associated actions.
## Table of Contents
- [Installation](#installation)
- [Usage](#usage)
- [Development](#development)
- [Build](#build)
- [License](#license)
## Installation
To install the dependencies, run:
```bash
npm install
```
## Usage
To start the development server, run:
```bash
npm run dev
```
This will start a Vite development server and open the example app in your default browser.
### Registering a Shortcut
To register a keyboard shortcut, use the `keyboardShortcutSDK().register` method. For example:
```ts
import { keyboardShortcutSDK } from "../src"
keyboardShortcutSDK().register(
"ctrl+s",
() => {
alert("Saved!")
},
{
feedback: 'Shortcut "ctrl+s" registered',
}
)
```
### `register` Method
The `register` method allows you to register a custom keyboard shortcut that can trigger specific actions within your application.
#### Parameters
- `shortcut` (string): The keyboard shortcut to register (e.g., 'Ctrl+S').
- `callback` (function): The function to be executed when the shortcut is pressed.
- `options` (object, optional): Optional settings for the shortcut registration.
- `condition` (() => boolean, optional): If it returns true when the shortcut is called, the shortcut callback will run. Otherwise, the callback is not triggered.
- `feedback` (string, optional): A feedback string that will appear in a toast when registering the shortcut.
#### Examples
```ts
// Register a 'Ctrl+S' shortcut that saves the current document
KeyboardShortcutSDK.register('Ctrl+S', saveDocument);
// Register a 'Ctrl+Z' shortcut that undoes the last action, and has a feedback
KeyboardShortcutSDK.register('Ctrl+Z', undoLastAction, { feedback: "Ctrl+Z shortcut registered!" });
// Register a 'Ctrl+X' shortcut that removes the last action only if it exists
KeyboardShortcutSDK.register('Ctrl+X', removeLastAction, { condition: lastActionExists });
```
## Development
### Project Structure
- `src/`: Contains the source code for the SDK.
- `app/`: Contains the example application demonstrating the usage of the SDK.
- `dist/`: The output directory for the build process.
### Scripts
- `npm run dev`: Starts the development server.
- `npm run build`: Builds the project.
### TypeScript Configuration
The project uses TypeScript for type checking. The configuration files are:
- `tsconfig.json`: Main TypeScript configuration.
- `tsconfig.build.json`: TypeScript configuration for the build process.
## Build
To build the project, run:
```bash
npm run build
```
This will generate the output files in the `dist/` directory.
## License
This project is licensed under the MIT License.
Code language: JavaScript (javascript)
Some more good practices and additional information you should keep in mind when building an SDK:
docs
folder too.Framework-agnostic SDKs represent a leap toward inclusive and sustainable software development. By adhering to best practices and focusing on universal design principles, you can create tools that empower developers, regardless of their framework preferences.
Start building your framework-agnostic SDK today, and watch as your tool becomes a cornerstone of countless projects!
Paulo Nascimento | Apr 10, 2025