Build Once, Deploy Everywhere: Reusable Components
Stop rebuilding the same thing for every project.
The Problem
Every new project starts with the same setup work:
- Authentication and user management
- Database configuration and migrations
- CI/CD pipelines
- API patterns and error handling
- UI components (buttons, forms, modals)
- Deployment infrastructure
This isn’t creative work. It’s repetitive work that eats into the time you could spend on features that actually differentiate your product.
The cost: A typical full-stack project spends 30-40% of initial development time on foundational infrastructure that’s nearly identical to the last project.
The Solution: Component Libraries
Reusable components work at two levels:
- Infrastructure components - CDK constructs, Terraform modules, deployment patterns
- Design components - UI libraries, design tokens, component systems
Both follow the same principle: solve the problem once, reuse it everywhere.
Infrastructure Components with CDK
Here’s a real example. Every web app needs a standard setup: CloudFront distribution, S3 bucket for static assets, proper caching headers, and HTTPS.
Instead of writing this for every project:
// Without reusable components: 80+ lines per project
const bucket = new s3.Bucket(this, "Frontend", {
// ... 15 lines of config
});
const distribution = new cloudfront.Distribution(this, "CDN", {
// ... 50+ lines of behaviors, origins, caching
});
const deployment = new s3deploy.BucketDeployment(this, "Deploy", {
// ... 15 lines of deployment config
});
Create a reusable construct:
// packages/constructs/src/static-site.ts
export class StaticSite extends Construct {
public readonly bucket: s3.Bucket;
public readonly distribution: cloudfront.Distribution;
constructor(scope: Construct, id: string, props: StaticSiteProps) {
super(scope, id);
this.bucket = new s3.Bucket(this, "Bucket", {
removalPolicy: props.removalPolicy ?? RemovalPolicy.RETAIN,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
});
this.distribution = new cloudfront.Distribution(this, "Distribution", {
defaultBehavior: {
origin: new origins.S3Origin(this.bucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
},
domainNames: props.domainName ? [props.domainName] : undefined,
certificate: props.certificate,
defaultRootObject: "index.html",
errorResponses: [
{
httpStatus: 404,
responseHttpStatus: 200,
responsePagePath: "/index.html", // SPA routing
},
],
});
}
}
Now every project gets a production-ready static site in 3 lines:
const site = new StaticSite(this, "Frontend", {
domainName: "app.example.com",
certificate: cert,
});
Time saved per project: 2-4 hours of infrastructure work, plus avoided debugging of edge cases you’ve already solved.
Composing Larger Patterns
Individual components combine into full application patterns:
export class WebAppStack extends Stack {
constructor(scope: Construct, id: string, props: WebAppProps) {
super(scope, id, props);
// Reusable components snap together
const database = new PostgresDatabase(this, "Database", {
instanceSize: props.instanceSize,
vpc: props.vpc,
});
const api = new FargateApi(this, "Api", {
vpc: props.vpc,
database: database,
image: props.apiImage,
});
const frontend = new StaticSite(this, "Frontend", {
domainName: props.domainName,
certificate: props.certificate,
});
// Wire them together
new ApiGatewayIntegration(this, "Gateway", {
api: api,
frontend: frontend,
});
}
}
A complete web application infrastructure in 25 lines instead of 500.
Design System Components
The same principle applies to frontend code. Instead of styling buttons differently in every project:
// components/Button.tsx
interface ButtonProps {
variant: "primary" | "secondary" | "danger";
size: "sm" | "md" | "lg";
loading?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
export function Button({ variant, size, loading, children, onClick }: ButtonProps) {
return (
<button
className={clsx(
"rounded font-medium transition-colors",
variants[variant],
sizes[size],
loading && "opacity-50 cursor-not-allowed"
)}
onClick={onClick}
disabled={loading}
>
{loading ? <Spinner size={size} /> : children}
</button>
);
}
This button handles:
- Consistent styling across the app
- Loading states
- Accessibility (disabled when loading)
- Size and variant combinations
Build it once, use it everywhere. No more inconsistent buttons across pages.
What Makes a Good Reusable Component
Not everything should be a reusable component. Good candidates:
| Good for Reuse | Bad for Reuse |
|---|---|
| Authentication flows | Business-specific logic |
| Database setup | One-off integrations |
| Common UI patterns | Highly customized features |
| CI/CD pipelines | Project-specific workflows |
| Error handling | Domain models |
The rule: If you’ve built it twice, it’s a candidate for extraction. If it requires heavy customization per use, keep it project-specific.
Versioning and Publishing
For infrastructure components, publish to a private npm registry:
{
"name": "@yourorg/constructs",
"version": "1.2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts"
}
Projects pin to specific versions:
{
"dependencies": {
"@yourorg/constructs": "^1.2.0"
}
}
When you improve a component (better defaults, bug fixes, new features), all projects benefit on their next update.
The Compound Effect
The real value isn’t in any single component. It’s in the compound effect over time:
| Project | Without Library | With Library | Savings |
|---|---|---|---|
| Project 1 | 40 hours | 40 hours (building library) | 0 |
| Project 2 | 40 hours | 8 hours | 32 hours |
| Project 3 | 40 hours | 6 hours | 34 hours |
| Project 4 | 40 hours | 4 hours | 36 hours |
| Total | 160 hours | 58 hours | 64% |
By project 4, infrastructure setup takes a morning instead of a week.
Getting Started
If you don’t have a component library yet:
- Audit your last 3 projects. What code was nearly identical?
- Pick the most repeated pattern. Authentication, database setup, or deployment are common starting points.
- Extract it. Make it configurable but not over-engineered.
- Use it in your next project. Iterate based on real usage.
Don’t try to build a comprehensive library upfront. Let it grow from actual needs.
Lessons Learned
-
Start small. One well-designed component beats ten half-baked ones.
-
Document the why. Future you (or your team) needs to know why decisions were made.
-
Version aggressively. Breaking changes happen. Semantic versioning prevents surprises.
-
Test the components, not just the projects. A bug in a shared component affects everything.
-
Don’t over-abstract early. Wait until you’ve used something three times before generalizing.
Building reusable components is an investment. The first project takes longer. Every project after that is faster, more consistent, and less error-prone.