DEV Community

AceToolz
AceToolz

Posted on

Building 20 Online Tools in 6 Months: Lessons in Rapid Development

Six months ago, I had a simple idea: create a collection of useful online tools that developers and everyday users actually need. Today, AceToolz has 20 different online tools and growing. Here's everything I learned about rapid development, technical decisions that saved months, and mistakes that cost weeks.

The Challenge: 20 Tools in 180 Days

The goal was ambitious but clear:

  • 5 Text Processing Tools: Case converter, word counter, text diff, grammar checker, paraphraser
  • 7 PDF Tools: Editor, converter, compressor, merger, splitter, OCR, page management
  • 4 Image Tools: Compressor, resizer, format converter, background remover
  • 4 Generator Tools: QR codes, passwords, UUIDs, random patterns

Each tool needed to be production-ready, not a prototype.

Month 1: Foundation and Architecture Decisions

The Tech Stack Choice

// The foundation that made everything possible
const techStack = {
  framework: 'Next.js 14 (App Router)',
  database: 'Neon PostgreSQL + Prisma',
  authentication: 'NextAuth.js v5',
  hosting: 'Vercel',
  styling: 'Tailwind CSS',
  ui: 'Custom components (no external libraries)',
};
Enter fullscreen mode Exit fullscreen mode

Why these choices mattered:

  • Next.js App Router: Built-in API routes eliminated backend complexity
  • Serverless: Zero infrastructure management
  • Prisma: Database changes without migrations hell
  • TypeScript: Caught bugs before users did

The Tool Interface Pattern

The biggest time-saver was standardizing every tool:

// lib/tools/interface.ts
export interface Tool {
  name: string;
  slug: string;
  description: string;
  category: 'text' | 'pdf' | 'image' | 'generate';
  isPremium: boolean;
  processingType: 'client' | 'server';
  settings: ToolSetting[];
  process: (input: any, settings: any) => Promise<any>;
}

// Example implementation
export const wordCounterTool: Tool = {
  name: 'Word Counter',
  slug: 'word-counter',
  description: 'Count words, characters, and paragraphs',
  category: 'text',
  isPremium: false,
  processingType: 'client',
  settings: [
    {
      key: 'includeSpaces',
      type: 'boolean',
      label: 'Include spaces in character count',
      defaultValue: true,
    }
  ],
  process: async (text: string, settings) => {
    return {
      words: text.trim().split(/\s+/).filter(Boolean).length,
      characters: settings.includeSpaces ? text.length : text.replace(/\s/g, '').length,
      paragraphs: text.split('\n\n').filter(Boolean).length,
    };
  },
};
Enter fullscreen mode Exit fullscreen mode

This pattern saved weeks. Every tool followed the same structure.

The Reusable Component System

// components/tools/ToolContainer.tsx
interface ToolContainerProps {
  tool: Tool;
  children: React.ReactNode;
  onProcess: (input: any) => Promise<any>;
}

export default function ToolContainer({ tool, children, onProcess }: ToolContainerProps) {
  const [loading, setLoading] = useState(false);
  const [result, setResult] = useState(null);

  const handleProcess = async (input: any) => {
    setLoading(true);
    try {
      const result = await onProcess(input);
      setResult(result);
    } catch (error) {
      setError(error.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="tool-container">
      <ToolHeader tool={tool} />
      <div className="tool-body">
        {children}
      </div>
      <ToolFooter loading={loading} result={result} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Month 2-3: The Text and Generator Tools Sprint

Client-Side vs Server-Side Decision Matrix

Early on, I created a decision framework:

const processingDecision = {
  clientSide: [
    'Simple text manipulation',
    'No external APIs needed',
    'Privacy-sensitive operations',
    'Instant feedback required'
  ],
  serverSide: [
    'External API integration',
    'Heavy computation',
    'File processing',
    'Premium features'
  ]
};
Enter fullscreen mode Exit fullscreen mode

Text Tools: The Fast Wins

Text tools were perfect for building momentum:

// lib/tools/caseConverter.ts - Built in 1 day
export async function convertCase(text: string, targetCase: string) {
  switch (targetCase) {
    case 'uppercase':
      return text.toUpperCase();
    case 'lowercase':
      return text.toLowerCase();
    case 'titlecase':
      return text.replace(/\w\S*/g, (txt) => 
        txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
      );
    case 'camelcase':
      return text.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => 
        index === 0 ? word.toLowerCase() : word.toUpperCase()
      ).replace(/\s+/g, '');
    default:
      return text;
  }
}
Enter fullscreen mode Exit fullscreen mode

Key insight: Start with simple, high-value tools to build confidence and establish patterns.

The Generator Tools Pattern

// lib/tools/passwordGenerator.ts - Reusable randomization
class SecureRandomizer {
  private static charSets = {
    lowercase: 'abcdefghijklmnopqrstuvwxyz',
    uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
    numbers: '0123456789',
    symbols: '!@#$%^&*()_+-=[]{}|;:,.<>?'
  };

  static generatePassword(options: PasswordOptions): string {
    const charset = this.buildCharset(options);
    const array = new Uint32Array(options.length);
    crypto.getRandomValues(array);

    return Array.from(array, x => charset[x % charset.length]).join('');
  }

  static generateUUID(): string {
    return crypto.randomUUID();
  }

  // Reused across 3 different generator tools
  private static buildCharset(options: any): string {
    let charset = '';
    Object.entries(options).forEach(([key, enabled]) => {
      if (enabled && this.charSets[key]) {
        charset += this.charSets[key];
      }
    });
    return charset;
  }
}
Enter fullscreen mode Exit fullscreen mode

Month 3-4: PDF Tools - The Real Challenge

The Vercel Serverless Limitation

Hit a wall immediately:

// This failed - Vercel has 4.5MB request limit
export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const file = formData.get('file') as File; // 50MB PDF = 💥

  // Process file... CRASH
}
Enter fullscreen mode Exit fullscreen mode

The External API Solution

// lib/utils/ilovepdf-upload.ts
import ILovePDFApi from '@ilovepdf/ilovepdf-nodejs';

class PDFProcessor {
  private api: ILovePDFApi;

  constructor() {
    this.api = new ILovePDFApi(
      process.env.ILOVEPDF_PUBLIC_KEY!,
      process.env.ILOVEPDF_SECRET_KEY!
    );
  }

  async compressPDF(fileBuffer: Buffer): Promise<Buffer> {
    const task = this.api.newTask('compress');

    try {
      await task.start();
      const file = await task.addFile(fileBuffer);
      await task.process();
      return await task.download();
    } finally {
      await task.delete(); // Cleanup
    }
  }

  async convertToPDF(fileBuffer: Buffer, fromFormat: string): Promise<Buffer> {
    const task = this.api.newTask('officepdf');
    // Similar pattern...
  }
}
Enter fullscreen mode Exit fullscreen mode

The PDF.js Integration for Editing

// components/tools/PDFEditor/PDFEditorCanvas.tsx
import * as pdfjsLib from 'pdfjs-dist';

export default function PDFEditorCanvas({ pdfData }: { pdfData: ArrayBuffer }) {
  const [pdf, setPdf] = useState<PDFDocumentProxy | null>(null);
  const [currentPage, setCurrentPage] = useState(1);

  useEffect(() => {
    const loadPDF = async () => {
      const loadingTask = pdfjsLib.getDocument(pdfData);
      const pdfDoc = await loadingTask.promise;
      setPdf(pdfDoc);
    };

    loadPDF();
  }, [pdfData]);

  const renderPage = async (pageNumber: number) => {
    if (!pdf) return;

    const page = await pdf.getPage(pageNumber);
    const scale = 1.5;
    const viewport = page.getViewport({ scale });

    const canvas = canvasRef.current!;
    const context = canvas.getContext('2d')!;
    canvas.height = viewport.height;
    canvas.width = viewport.width;

    const renderContext = {
      canvasContext: context,
      viewport: viewport
    };

    await page.render(renderContext).promise;
  };

  // Annotation system, text editing, shape tools...
}
Enter fullscreen mode Exit fullscreen mode

Lesson learned: For complex features like PDF editing, don't reinvent the wheel. PDF.js + Canvas API provided 90% of the functionality.

Month 4-5: Image Tools and External Services

ImageKit.io Integration

// lib/utils/imagekit/client.ts
import ImageKit from 'imagekit';

class ImageProcessor {
  private imagekit: ImageKit;

  constructor() {
    this.imagekit = new ImageKit({
      publicKey: process.env.NEXT_PUBLIC_IMAGEKIT_PUBLIC_KEY!,
      privateKey: process.env.IMAGEKIT_PRIVATE_KEY!,
      urlEndpoint: process.env.NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT!,
    });
  }

  async compressImage(file: File, quality: number): Promise<string> {
    const uploadResult = await this.imagekit.upload({
      file: file,
      fileName: `compressed_${Date.now()}`,
      folder: '/temp-images/',
    });

    // Generate compressed URL
    return this.imagekit.url({
      path: uploadResult.filePath,
      transformation: [{
        quality: quality.toString(),
        format: 'auto'
      }]
    });
  }

  async removeBackground(file: File): Promise<string> {
    // Upload and apply background removal
    const uploadResult = await this.imagekit.upload({
      file: file,
      fileName: `nobg_${Date.now()}`,
      folder: '/temp-images/',
    });

    return this.imagekit.url({
      path: uploadResult.filePath,
      transformation: [{
        effect: 'bg-removal'
      }]
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The Usage Tracking System

// lib/utils/user-limits.ts
export class UsageLimiter {
  static async checkLimit(userId: string, toolSlug: string): Promise<boolean> {
    const today = new Date().toISOString().split('T')[0];

    const usage = await prisma.toolUsage.findFirst({
      where: {
        userId,
        toolSlug,
        date: today,
      }
    });

    const userRole = await this.getUserRole(userId);
    const limit = this.getLimitForRole(userRole, toolSlug);

    return !usage || usage.count < limit;
  }

  static async incrementUsage(userId: string, toolSlug: string): Promise<void> {
    const today = new Date().toISOString().split('T')[0];

    await prisma.toolUsage.upsert({
      where: {
        userId_toolSlug_date: {
          userId,
          toolSlug,
          date: today,
        }
      },
      update: {
        count: { increment: 1 }
      },
      create: {
        userId,
        toolSlug,
        date: today,
        count: 1,
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Month 6: Performance, Polish, and Launch

The Ocean Depth Design System

/* globals.css - The visual identity */
:root {
  --primary-gradient: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 50%, #06b6d4 100%);
  --morning-sky: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 50%, #93c5fd 100%);
  --surface-gradient: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
  --accent-blue: #2563eb;
  --ocean-deep: #1e40af;
}

.tool-container {
  background: var(--surface-gradient);
  border-radius: 24px;
  box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1);
}

.primary-button {
  background: var(--primary-gradient);
  border-radius: 16px;
  transition: all 0.3s ease;
}
Enter fullscreen mode Exit fullscreen mode

Performance Optimizations

// Performance monitoring wrapper
export function withPerformanceMonitoring<T extends (...args: any[]) => any>(
  fn: T,
  operationName: string
): T {
  return ((...args: any[]) => {
    const start = performance.now();

    const result = fn(...args);

    if (result instanceof Promise) {
      return result.finally(() => {
        const end = performance.now();
        console.log(`${operationName} took ${end - start} milliseconds`);
      });
    } else {
      const end = performance.now();
      console.log(`${operationName} took ${end - start} milliseconds`);
      return result;
    }
  }) as T;
}

// Applied to all tool processing functions
export const optimizedWordCounter = withPerformanceMonitoring(
  wordCounterTool.process,
  'Word Counter Processing'
);
Enter fullscreen mode Exit fullscreen mode

The Numbers: What Actually Happened

Development Timeline

  • Month 1: Foundation + 3 tools (text tools)
  • Month 2: 8 tools (remaining text + all generators)
  • Month 3: 4 tools (basic PDF tools)
  • Month 4: 3 tools (complex PDF tools)
  • Month 5: 4 tools (image tools)
  • Month 6: Polish + performance + launch

Technical Metrics

  • Page Load Time: <2 seconds (average)
  • Tool Processing: 85% under 5 seconds
  • Uptime: 99.9%
  • Bundle Size: 125KB (gzipped)

Biggest Lessons Learned

1. Start Simple, Scale Complexity

// DON'T start with this complexity
const advancedPDFEditor = {
  features: ['annotations', 'forms', 'signatures', 'collaboration'],
  complexity: 'insane',
  timeToMarket: '6 months'
};

// START with this
const basicPDFViewer = {
  features: ['view', 'download'],
  complexity: 'manageable', 
  timeToMarket: '1 week'
};
Enter fullscreen mode Exit fullscreen mode

2. External APIs > Reinventing Wheels

Building PDF processing from scratch would have taken 6 months. Using iLovePDF API took 6 days.

3. User Feedback Drives Features

The most-used tools were the ones I thought were "too simple":

  1. Word Counter: 40% of all usage
  2. Case Converter: 25% of all usage
  3. QR Generator: 20% of all usage

4. Performance Matters More Than Features

// This optimization saved the project
const optimizations = {
  'Lazy loading components': '+40% page speed',
  'Client-side processing': '+60% user satisfaction',
  'Proper caching': '+30% server efficiency',
  'Image optimization': '+25% mobile performance'
};
Enter fullscreen mode Exit fullscreen mode

5. Documentation Your Future Self Will Thank You For

/**
 * PDF Processing Pipeline
 * 
 * Flow: Upload -> Validate -> Process -> Download
 * Limits: 50MB max file size
 * Supported: PDF, DOC, DOCX, PPT, PPTX
 * 
 * @example
 * const result = await processPDF(file, 'compress', { quality: 0.8 });
 */
export async function processPDF(
  file: File, 
  operation: PDFOperation, 
  options: ProcessingOptions
): Promise<ProcessingResult> {
  // Implementation...
}
Enter fullscreen mode Exit fullscreen mode

What I'd Do Differently

1. Start with Analytics Day 1

I added analytics in month 3. Wish I had usage data from the beginning.

2. Build Premium Features Earlier

Free tier limitations came in month 4. Should have been part of the original architecture.

3. More Aggressive Caching

// Implemented too late
const cacheStrategy = {
  'Tool results': 'Redis, 1 hour',
  'User preferences': 'Local storage',
  'Static assets': 'CDN, 30 days'
};
Enter fullscreen mode Exit fullscreen mode

The Tech Stack That Delivered

MVP Speed Winners:

  • Next.js App Router (pages in minutes)
  • Vercel deployment (zero config)
  • Prisma (database changes without fear)
  • Tailwind CSS (styling without custom CSS)

Production Scale Heroes:

  • ImageKit.io (handled 1M+ image operations)
  • iLovePDF (processed 100K+ PDFs)
  • Neon PostgreSQL (serverless database that just works)
  • NextAuth.js (authentication without security headaches)

The Current State

  • 20 production tools across 4 categories
  • 99.9% uptime (Vercel is amazing)
  • Sub-2-second load times globally
  • Zero security incidents (thanks NextAuth + Vercel)

What's Next

Currently building:

  • Browser Extension: Bring tools to where users work
  • API Service: Let developers integrate our tools
  • Mobile Apps: Native iOS/Android applications
  • Workflow Builder: Chain tools together

Try the tools yourself: AceToolz.com


Building something similar? I'd love to hear about your approach in the comments!

Top comments (0)