Lessions from AI coding

Published on
Last updated on
Technical
AI

Design

Architecure

  • Client Application

    • Unified data schema across the whole application, including:

      • Database (file-based) models and configuration files
      • API payloads
      • Client-side type definitions
    • Event-based propagation for notifications across the whole app:

      • A central event bus is used to send and receive events.
      • Components can subscribe to specific events and react without being directly coupled to the event source.
    • Hot reload for seamless updates:

      • Modules and configuration can be updated on-the-fly without restarting the entire application.
      • This significantly improves the development workflow and allows for dynamic adjustments in production.
    • Clear directory hierarchy for different packages (ui/functions/versioned api):

      • No mix or overlap
      • Grouped by function
  • Web Application

    • Versioned API with a clear, predictable hierarchy: align API routes with the source code and package structure so routes reflect implementation and versioning.

      • Group similar endpoints under a unified sub-path (e.g., /api/v1/users/, /api/v1/products/) to improve discoverability, routing consistency, and isolated versioning.
    • UI hierarchy aligned with code/package structure

UI/UX

Google Stitch is a good tool for design. As I don’t have practical experience with Figma, I can talk with Stitch to convey the requirements, and it can create up to 3 drafts. Stich Now has a redesign feature: you can paste a screenshot of the current design, and it can improve your design for you. If you are satisfied with the result, you can select the design and ask Stitch to prototype; then it can create an interactive UI for you. And of course, you can export it to Google AI Studio or copy it to the clipboard.

UI/UX design is not isolated from the rest of the architecture; it must be considered as part of the whole system.

  • If a UI component can be used in more than one place, create it as a shared component.
  • Prefer shadcn/ui and Tailwind CSS—not just for aesthetics, but because they are easier for AI-assisted development to reason about. Tailwind uses human-readable utility names rather than opaque technical terms, which suits AI coding well.

Authentication and Authorization

In the past 2 weeks, I am working on the MaCo MTool dashboard. Initially, I developed it as a monolithic application, including frontend/auth-service/backend-service as one big app. Since Fred pushed this tool to other teams, managers acknowledged that we still need this tool. We will make it an official tool across the MaCo team, so I have to split them to improve potential performance once we add more features. So I split this app.

  • Permission design

    • Permissions for each API call; each has Read (GET), Write (POST/PUT), and Delete (DEL), so there is a long list of permissions seeded to the database when initializing the database.

    • Group into 3 global roles

      • Admin role: it has all permissions.
      • Editor has Read and Write permissions.
      • Viewer has Read permissions.
    • User / resource groups

      • Add users to the groups.
      • Add resources to the groups.
      • Users in the group can access the resources in that group.
      • Admin users will bypass the group constraints.

    The frontend uses Next.js 16 and better-auth. Initially I used next-auth, but it is coupled with Next.js and cannot run in a standalone service, so I switched to better-auth, which supports multiple authentication methods and email/phone as authentication.

  • Authorization with custom-session

    When a user makes each service call to the backend, it will call auth-service/api/v1/auth/get-session, which will return user info plus roles and permissions, but actually it only returns the roleId, and we have two levels of permissions. The default get-session can’t provide such group-level info, so we need to use a custom-session plugin, so the whole response contains the detailed permission list.

    const auth = betterAuth({
    // ...other config
    plugins: [
    // ...other plugins
    customSession(async ({ user, session }) => {
    // enrich session with roles/permissions/groups
    })
    ]
    })

    And in the backend service, when it receives any request, it first verifies the permission and group to determine if it will be rejected as 403 or continue processing. In that group scope, do the operations without breaking the boundaries.

Database

  • When user info is stored in a User table, and other entities such as a product table also reference the userId or userName from the User table, but we can’t allow users to have authority to list all users, how to do this?

    My consideration:

    1. Store the auth info and the business logic in the same database.

    2. When any user creates the entity, it will save the userId and userName along with the entity in the table. Other users querying the entity can always know who created this entity, but there may be inconsistencies when the creator changes their name, but currently it should be OK.

    3. If auth and business are in separate databases, we can also sync the user table to the business table.

      • Shows current names
      • No user:read permission needed for regular users
      • Fast - local DB lookup
      • Handles renames - webhook updates cache
      • Ready for separate DBs
      • Industry-standard pattern

      And auth-servce to backend-service communicates via HMAC (Hash-based Message Authentication Code) which is is a cryptographic mechanism to verify:

      1. Authenticity - The request came from a trusted source (auth-service)
      2. Integrity - The payload wasn’t tampered with in transit
      3. the sync process will also use HAMC to do the data sync

Code Rules

  • I’ve collected many rules; they’re integrated into super-dev-plugin and my CLAUDE.md, and I will continue improving them.

  • For variables, define them in advance and use clear, human-readable names that describe their purpose and function. Follow consistent naming conventions, limit scope to the smallest necessary, prefer immutable constants where applicable, and document any non-obvious choices or side effects.

git worktree

We can use git worktree to implement a feature. Since we are human and a design may not be fully covered, especially if a feature is more complicated, we can use a git worktree to do a draft implementation. If there are any issues, we can do new research and assessment, update the spec, and create a new worktree to do a second version. We can iterate through multiple rounds to finally get good code, then merge it into the main development branch.

  • Before starting a new development, first create a requirement and do various research to create a spec called 01-draft-spec.md.
  • Create a git worktree: mkdir -p .worktree && git worktree add .worktree/spec-180-impl-01 -b spec-180-impl-01.
  • Add .worktree to .gitignore.
  • Change directory to .worktree/spec-180-impl-01 and start to implement.
  • After implementation, test and verify if it works as expected.
    • If it does, change to the main branch (git checkout main), merge spec-180-impl-01, and then remove the worktree: git worktree remove .worktree/spec-180-impl-01.
    • If it is not working well, do research again, update the spec, and then repeat the git worktree process.
  • We can even create multiple worktrees simultaneously and use different code agents to implement, only merging the one which is best.
Back to Blog