Sesam.hu — Engineering Manager & Trail Runner

Sesam.hu

Personal blog by Péter Szilágyi — Engineering Manager and trail runner based in Stockholm.
Writing on leadership, running, and life in the north.

English

Lovable 💖 + Kiro 👻

The title is slightly misleading, because this post is going to be about me learning more TypeScript and React. I think.

It all started with AWS organizing Dev Days in Stockholm, and since I attended the Nordic Tech Week earlier, I signed up, even though I wasn't sure it was for me… Impostor syndrome aside, of course it proved to be awesome, and I got to see two demo sessions where the presenters used Kiro, AWS's new IDE, henceforth be known as 👻, to build apps from scratch showcasing (among other things) spec-driven development and the new Kiro CLI. Kiro actually went GA only two days earlier.

I really didn't use any of the shiny new features other than the gifted 500 credits, the automatic selection of language model, and the Autopilot mode, where one mostly lets the agent run free and enjoy the results. (Writing this, I just realized that Steering would actually have been extremely useful. Added to todo list.)

Anyway, I went ahead and asked Kiro to:

"Examine this codebase written by a junior developer for software development best practices, clean code, security, possible improvements."

She started screaming that a high priority issue was the .env file being on GitHub, which is actually needed for the Lovable sync, so I eventually had to admit that there was no junior developer, after all.

👻 went:

"Ah, that changes everything! Let me reframe this for a Lovable-generated codebase where you're doing hybrid development (Lovable web editor + local IDE)."

And so a plan was formed from which I learned more than I expected. Kiro (Claude, probably, agent was set to Auto,) proposed a sequential plan to follow from quick wins to improving code quality. Here's how it went.

Caveats:

  • Code by Kiro, problems/lessons by me.
  • This is very particular for this blog and my learning path, but might be interesting to look at regardless.

Day 1: The Quick Wins (90 Minutes)

The Toast That Wouldn't Leave

The Problem: Lovable, for some unfathomable reason, decided that toasts on the admin dashboard should persist eternally (?).

// Before: 1000000ms = 1000 seconds = 16+ minutes
const TOAST_REMOVE_DELAY = 1000000;

// After: 5000ms = 5 seconds
const TOAST_REMOVE_DELAY = 5000;

Lesson: Always validate timeout values, because agents can choose weird numbers.

The N+1 Query Trap

The Problem: At some point, because "it's always been so", I modified the index page to load the latest ten blog posts. This apparently triggered 11 database queries: one for posts, then one for each post's images.

The Solution: Kiro mentioned that modern database tools support joins and recommended using them.

// Before: 11 queries
const posts = await db.posts.findMany();
const postsWithImages = await Promise.all(
  posts.map(post => db.images.findMany({ where: { postId: post.id }}))
);

// After: 1 query with join
const posts = await db.posts.findMany({
  include: { images: true }
});

Impact: Kiro was claiming significantly faster page loads, although it was quick enough already, but I suppose this might be different for larger projects with more complex queries.

Lesson: Look for N+1 patterns when fetching related data.

Error Boundaries: The Safety Net

The Problem: Any component error crashed the entire app, leaving users with a blank white screen. This wasn't really an issue, because I'd have caught it early in the Lovable preview, but again, outside of this bubble things are different.

The Solution: React Error Boundaries provide graceful degradation.

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  render() {
    if (this.state.hasError) {
      return <FriendlyErrorMessage />;
    }
    return this.props.children;
  }
}

Lesson: Error boundaries are essential for production apps: users should never see a blank screen.

Professional Logging

The Problem: 30+ console.log() statements cluttering the production console, thanks to Lovable littering the code with them when asked to investigate why something broke.

The Solution: Environment-aware logging utility. Thanks, 👻.

export const logger = {
  log: (...args) => {
    if (process.env.NODE_ENV === 'development') {
      console.log(...args);
    }
  },
  error: console.error,  // Always log errors
  warn: console.warn,
  debug: (context, ...args) => {
    if (process.env.NODE_ENV === 'development') {
      console.log(`[${context}]`, ...args);
    }
  }
};

Lesson: Debug logs are valuable during development but unprofessional in production, so they should be gated appropriately.

Day 2: Performance & UX (2 Hours)

Code Splitting with Lazy Loading

The Problem: Admin dashboard code loaded for all users, even though I'm the only person accessing it.

The Solution: React's lazy loading with Suspense.

// Before: Always loaded
import AdminDashboard from './pages/AdminDashboard';

// After: Loaded on demand
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));

<Suspense fallback={<Loader />}>
  <AdminDashboard />
</Suspense>

Impact: Smaller initial bundle, faster first page load, even though it was quick enough already.

Lesson: Lazy load features that most users don't need, helps with the bundle size.

React Query: Caching Done Right

The Problem: Lovable opted for a manual state management with useState and useEffect, no caching, and repeated fetches.

The Solution: Using React Query to handle caching, background refetching, and loading states automatically.

// Before: ~100 lines of manual state management
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
  fetchPosts().then(data => {
    setPosts(data);
    setLoading(false);
  });
}, []);

// After: ~20 lines with automatic caching
const { data: posts, isLoading } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  staleTime: 5 * 60 * 1000,  // 5 minutes
});

Impact:

  • 80% less code
  • Automatic caching
  • Instant navigation between cached pages
  • Background refetching keeps data fresh

Lesson: Reinventing the wheel is probably pointless.

Image Optimization

The Problem: Loading full-resolution 2.5MB images when most likely the one shown was closer to a 200KB version.

The Solution: Image transformation at the CDN level.

// Add width and quality parameters
const optimizedUrl = `${imageUrl}?width=1200&quality=85`;

Impact: 92% (according to 👻) bandwidth savings, this time with faster page loads that are humanly discernible.

Lesson: Modern CDNs make optimizing images trivial with URL parameters.

Skeleton Loading States

The Problem: Lovable just added generic "Loading..." spinners.

The Solution: Content-matching skeleton screens that look way more professional, by the way.

// Instead of: <Spinner />
// Show structure that matches actual content:
<div className="space-y-4">
  <div className="h-8 bg-gray-200 animate-pulse rounded w-3/4" />
  <div className="h-4 bg-gray-200 animate-pulse rounded w-1/2" />
  <div className="h-64 bg-gray-200 animate-pulse rounded" />
</div>

Impact: Better perceived performance and avoiding layout shifts.

Lesson: Skeleton screens make loading feel faster and show users what's coming.

Centralized Constants

The Problem: Magic numbers were scattered throughout the codebase (10, 5 * 1024 * 1024, 200). Lovable tended to do this as she was focusing on the problem at hand and the individual files that were affected, not the entirety of the codebase.

The Solution: Single source of truth for configuration.

// config/constants.ts
export const POSTS_PER_PAGE = 10;
export const MAX_FILE_SIZE = 5 * 1024 * 1024;  // 5MB
export const EXCERPT_LENGTH = 200;
export const CACHE_STALE_TIME = 5 * 60 * 1000;  // 5 minutes

Lesson: Magic numbers are maintenance nightmares, so centralizing and documenting them are the way.

Day 3: Code Quality (4 Hours)

Standardized Error Handling

The Problem: Similarly to magic numbers, error handling was inconsistent: some places logged and continued, others threw, and some technical error messages leaked to visitors.

The Solution: Centralized error handling with user-friendly messages.

// Error handler with types
export class AppError extends Error {
  constructor(
    message: string,
    public type: ErrorType,
    public originalError?: unknown
  ) {
    super(message);
  }
}

export const handleError = (error: unknown, context: string): AppError => {
  logger.error(`[${context}]`, error);
  
  if (error instanceof AppError) return error;
  if (isNetworkError(error)) return new AppError('Network error', 'NETWORK');
  if (isAuthError(error)) return new AppError('Authentication failed', 'AUTH');
  
  return new AppError('Something went wrong', 'UNKNOWN');
};

export const getUserFriendlyMessage = (error: AppError): string => {
  const messages = {
    NETWORK: 'Connection problem. Please check your internet.',
    AUTH: 'Please sign in again.',
    NOT_FOUND: 'Content not found.',
    // ...
  };
  return messages[error.type] || 'Something went wrong. Please try again.';
};

Impact: Consistent error handling, better debugging, user-friendly messages.

Lesson: Standardize error handling early (and make your agent follow this pattern).

Custom Hooks for Reusability

The Problem: Data fetching logic duplicated across multiple components is a classic Lovable thing, according to yours truly. Once again, the fact that she focuses on particular problems can result in duplicated efforts.

The Solution: Extract into reusable custom hooks.

// Before: Inline in every component
const [posts, setPosts] = useState([]);
useEffect(() => {
  fetchPosts().then(setPosts);
}, []);

// After: Reusable hook
export const usePosts = (page: number) => {
  return useQuery({
    queryKey: ['posts', page],
    queryFn: () => fetchPosts(page),
    staleTime: CACHE_STALE_TIME,
  });
};

// Usage in component
const { data: posts, isLoading } = usePosts(currentPage);

Impact:

  • Removed ~150 lines of duplicate code
  • Single source of truth for data fetching
  • Easier testing
  • Consistent patterns

Lesson: Data fetching logic is best extracted into a custom hook, TIL.

TypeScript Strict Mode

The Problem: By default Lovable sets up a loose TypeScript configuration, which won't bother 99.9% of users, but this post is about code quality and best practices.

The Solution: Enable strict mode gradually. (Or just effin🐦 it and enable everything.)

{
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "strictNullChecks": true
  }
}

Result: Zero errors! The previous improvements had already made the codebase type-safe.

Lesson: Strict mode catches bugs before they reach production (is the promise I wait to see.)

So how about that? I enjoyed myself immensely while doing these, and asking the agent to stop before committing or pushing anything was very useful, because one could look at the changes and learn from them. Also, agents are very good at summarizing why they do something, which also proves to be helpful in an educational setting.

🎁 Get Kiro and play with the 500 initial gift credits!

Comments