Build Once, Deploy Everywhere: Reusable Components

Foundry24

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:

  1. Infrastructure components - CDK constructs, Terraform modules, deployment patterns
  2. 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 ReuseBad for Reuse
Authentication flowsBusiness-specific logic
Database setupOne-off integrations
Common UI patternsHighly customized features
CI/CD pipelinesProject-specific workflows
Error handlingDomain 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:

ProjectWithout LibraryWith LibrarySavings
Project 140 hours40 hours (building library)0
Project 240 hours8 hours32 hours
Project 340 hours6 hours34 hours
Project 440 hours4 hours36 hours
Total160 hours58 hours64%

By project 4, infrastructure setup takes a morning instead of a week.

Getting Started

If you don’t have a component library yet:

  1. Audit your last 3 projects. What code was nearly identical?
  2. Pick the most repeated pattern. Authentication, database setup, or deployment are common starting points.
  3. Extract it. Make it configurable but not over-engineered.
  4. 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

  1. Start small. One well-designed component beats ten half-baked ones.

  2. Document the why. Future you (or your team) needs to know why decisions were made.

  3. Version aggressively. Breaking changes happen. Semantic versioning prevents surprises.

  4. Test the components, not just the projects. A bug in a shared component affects everything.

  5. 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.