In modern software development, we often face a trade-off: the simplicity of a monolith versus the scalability of microservices. Monoliths are easy to start but become cumbersome to maintain and scale. Microservices offer scalability but introduce immense complexity in orchestration, deployment, and inter-service communication.
What if there was a third way? A pattern that combined the simplicity of writing a single piece of code with the scalability and resilience of a distributed system.
Welcome to the world of composable, atomic functions. At function.do, we believe this is the future of backend development. Instead of building monolithic applications or juggling complex microservice architectures, you can build powerful, scalable workflows by chaining together simple, single-purpose functions.
Let's explore how.
An atomic function is a small, self-contained unit of code designed to perform one specific task perfectly. It adheres to the Single Responsibility Principle. It doesn't know or care about the larger application; it just takes an input, performs its logic, and returns an output.
On the function.do platform, every function you export becomes its own dedicated, scalable serverless API endpoint.
Think of our getGreeting example:
import { Fn } from '@do-are/sdk';
/**
* @description Generates a personalized greeting.
* @param { name: string } - The name to include in the greeting.
* @returns { message: string } - The personalized greeting message.
*/
export const getGreeting: Fn = async ({ name }) => {
if (!name) {
throw new Error('A name is required to generate a greeting.');
}
const message = `Hello, ${name}! Welcome to the platform.`;
return { message };
};
// Deploys to: POST https://your-name.function.do/getGreeting
This function does one thing: it creates a greeting. It's easy to write, easy to test, and easy to understand. This radical simplicity is the foundation. While platforms like AWS Lambda provide the building blocks, function.do provides the complete, production-ready solution. You write the logic, and we instantly generate a secure, documented API. No boilerplate, no infrastructure management.
The true power emerges when you realize these atomic functions are not isolated islands. They are composable functions—building blocks for creating sophisticated systems.
Let's imagine a common real-world scenario: a user signup flow. This process involves several distinct steps:
Instead of bundling this into one giant signUp controller, we can create three distinct atomic functions.
This function’s only job is to check data. It doesn't touch a database or send an email.
//
// functions/validateInput.ts
//
import { Fn } from '@do-are/sdk';
export const validateInput: Fn = async ({ email, password }) => {
if (!email || !/^\S+@\S+\.\S+$/.test(email)) {
throw new Error('Invalid email format.');
}
if (!password || password.length < 8) {
throw new Error('Password must be at least 8 characters long.');
}
// If valid, return the data.
return { email, password };
};
This function assumes it's receiving valid data. Its only concern is interacting with the database. It can even use external NPM packages like a database driver. Just add pg, mysql2, or @supabase/supabase-js to your package.json, and function.do handles the rest.
//
// functions/createUser.ts
//
import { Fn } from '@do-are/sdk';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_KEY!);
export const createUser: Fn = async ({ email, password }) => {
// We trust the input has been validated by a previous function.
const { data, error } = await supabase.auth.signUp({ email, password });
if (error) {
throw new Error(`Database error: ${error.message}`);
}
return { userId: data.user?.id };
};
This function's responsibility is to send an email. It knows nothing about validation or user creation, only that it needs a userId to do its job.
//
// functions/sendWelcomeEmail.ts
//
import { Fn, Do } from '@do-are/sdk';
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export const sendWelcomeEmail: Fn = async ({ email, name }) => {
await resend.emails.send({
from: 'onboarding@your-app.com',
to: email,
subject: 'Welcome!',
html: `<h1>Hi ${name}, welcome to our platform!</h1>`,
});
return { status: 'sent' };
};
We now have three independent, reusable, and individually scalable microservices. How do we chain them? With another function! We'll create a primary signUp endpoint that orchestrates calls to the other functions.
function.do provides a powerful SDK to call other functions within your account easily.
//
// functions/signUp.ts
//
import { Fn, Do } from '@do-are/sdk';
export const signUp: Fn = async ({ email, password, name }) => {
try {
// 1. Call the validation function
await Do('validateInput', { email, password });
// 2. Call the user creation function
const { userId } = await Do('createUser', { email, password });
// 3. Call the email function (no need to wait for it)
Do('sendWelcomeEmail', { email, name }).catch(console.error);
// 4. Return the final success response to the client
return {
success: true,
message: 'User created successfully. Please check your email.',
userId: userId,
};
} catch (error: any) {
// If any step fails, the error is caught and returned.
throw new Error(`Signup failed: ${error.message}`);
}
};
Your frontend application now has a single, clean API endpoint to call:
POST https://your-name.function.do/signUp
This composable, Function as a Service model provides incredible advantages:
By transforming every piece of logic into a secure, ready-to-use API, function.do lets you move faster, build more resilient systems, and focus on what truly matters: your code.
Ready to build your next backend with atomic, composable functions?