How AI Helped Me Build a CRM Integration 60% Faster: A Real Vue.js + CouchDB Case Study
Ivan

Ivan @ivanrochacardoso

About: like programming

Location:
Nazaré Paulista, Brasil
Joined:
Mar 8, 2024

How AI Helped Me Build a CRM Integration 60% Faster: A Real Vue.js + CouchDB Case Study

Publish Date: Jun 26
0 0

By Ivan Rocha Cardoso - Full-Stack Developer at SiGlobal

The Challenge Every Developer Has Faced

Last week I got that request that makes any developer sigh deeply:

"Ivan, we need customers to register themselves in our CRM, but the data needs to be validated and automatically integrated with SIADM."

For context about our systems at SiGlobal:

  • SIADM: Our main CRM built with Quasar Framework + CouchDB
  • PreencheFácil: System that allows customer/member self-registration

Traditionally, this integration would take at least 2 weeks:

  • Integrate Vue.js/Quasar frontend with CouchDB
  • Implement complex validations (Tax ID, email, phone)
  • Configure CouchDB authentication
  • Error handling and data synchronization

Spoiler: I finished in 4 days. And no, I didn't become a coding machine. I used Cursor AI.


My First "Serious" Experience with AI Coding

I'd heard about Cursor before, but always thought: "Oh, just another fancy autocomplete".

Cursor isn't just code suggestions. It's like having a senior dev beside you who:

  • Understands your entire project context
  • Suggests architectures
  • Generates functional code (not just snippets)
  • Explains what it's doing

My Initial Setup

# Quasar + CouchDB integration project
npm install -g @quasar/cli
quasar create preenchefacil-integration

# Integration dependencies
npm install pouchdb-authentication pouchdb-memory pouchdb-adapter-http

# Cursor AI understood the stack just by seeing:
# - quasar.config.js
# - CouchDB connection configs
Enter fullscreen mode Exit fullscreen mode

Where Cursor AI Really Shined

1. API Structure Generation

My prompt:

I need to integrate a Quasar app (PreencheFácil) with CouchDB (SIADM):
- Vue component that collects registration data
- Tax ID, email and phone validation on frontend
- CouchDB sync using native authentication
- Document conflict handling
- Responsive interface with Quasar components
Enter fullscreen mode Exit fullscreen mode

What Cursor generated in 2 minutes:

<template>
  <q-page class="flex flex-center">
    <q-card class="q-pa-lg" style="width: 100%; max-width: 500px;">
      <q-card-section>
        <div class="text-h6 text-center">Customer Registration</div>
      </q-card-section>

      <q-card-section>
        <q-form @submit="onSubmit" class="q-gutter-md">
          <q-input
            v-model="form.name"
            label="Full Name"
            :rules="[val => !!val || 'Name is required']"
            outlined
          />

          <q-input
            v-model="form.email"
            label="Email"
            type="email"
            :rules="emailRules"
            outlined
          />

          <q-input
            v-model="form.tax_id"
            label="Tax ID"
            mask="###.###.###-##"
            :rules="taxIdRules"
            outlined
          />

          <q-input
            v-model="form.phone"
            label="Phone"
            mask="(##) #####-####"
            :rules="[val => !!val || 'Phone is required']"
            outlined
          />

          <q-btn
            label="Register"
            type="submit"
            color="primary"
            class="full-width"
            :loading="loading"
          />
        </q-form>
      </q-card-section>
    </q-card>
  </q-page>
</template>

<script>
import { ref } from 'vue'
import { useQuasar } from 'quasar'
import PouchDB from 'pouchdb'

export default {
  name: 'CustomerRegistration',
  setup() {
    const $q = useQuasar()
    const loading = ref(false)

    // CouchDB connection
    const db = new PouchDB('http://admin:password@localhost:5984/siadm')

    const form = ref({
      name: '',
      email: '',
      tax_id: '',
      phone: ''
    })

    // Validations
    const emailRules = [
      val => !!val || 'Email is required',
      val => /.+@.+\..+/.test(val) || 'Email must be valid'
    ]

    const taxIdRules = [
      val => !!val || 'Tax ID is required',
      val => validateTaxId(val) || 'Invalid Tax ID'
    ]

    // Tax ID validation (Brazilian CPF/CNPJ)
    function validateTaxId(value) {
      const cleanValue = value.replace(/\D/g, '')
      if (cleanValue.length === 11) {
        return validateCPF(cleanValue)
      } else if (cleanValue.length === 14) {
        return validateCNPJ(cleanValue)
      }
      return false
    }

    function validateCPF(cpf) {
      if (cpf.length !== 11 || /^(\d)\1+$/.test(cpf)) return false

      let sum = 0
      for (let i = 0; i < 9; i++) {
        sum += parseInt(cpf[i]) * (10 - i)
      }
      let remainder = sum % 11
      let digit1 = remainder < 2 ? 0 : 11 - remainder

      if (parseInt(cpf[9]) !== digit1) return false

      sum = 0
      for (let i = 0; i < 10; i++) {
        sum += parseInt(cpf[i]) * (11 - i)
      }
      remainder = sum % 11
      let digit2 = remainder < 2 ? 0 : 11 - remainder

      return parseInt(cpf[10]) === digit2
    }

    async function onSubmit() {
      loading.value = true

      try {
        // Check if customer already exists
        const existingDocs = await db.find({
          selector: {
            $or: [
              { email: form.value.email },
              { tax_id: form.value.tax_id }
            ]
          }
        })

        if (existingDocs.docs.length > 0) {
          $q.notify({
            type: 'negative',
            message: 'Customer already registered!'
          })
          return
        }

        // Create document in CouchDB
        const customerDoc = {
          _id: new Date().toISOString() + '_' + Math.random().toString(36).substr(2, 9),
          type: 'customer',
          name: form.value.name,
          email: form.value.email,
          tax_id: form.value.tax_id.replace(/\D/g, ''),
          phone: form.value.phone,
          created_at: new Date().toISOString(),
          status: 'active'
        }

        await db.put(customerDoc)

        $q.notify({
          type: 'positive',
          message: 'Customer registered successfully!'
        })

        // Reset form
        form.value = {
          name: '',
          email: '',
          tax_id: '',
          phone: ''
        }

      } catch (error) {
        console.error('Registration error:', error)
        $q.notify({
          type: 'negative',
          message: 'Registration failed. Please try again.'
        })
      } finally {
        loading.value = false
      }
    }

    return {
      form,
      loading,
      emailRules,
      taxIdRules,
      onSubmit
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

** My reaction:** "Dude, this has validation, masks, CouchDB integration and it's responsive!"

2. CouchDB Configuration That Would Take Me Hours

I asked: "How to configure authentication and synchronization with CouchDB for this use case?"

Cursor generated a complete Vue composable:

// composables/useCouchDB.js
import { ref } from 'vue'
import PouchDB from 'pouchdb'

export function useCouchDB() {
  const loading = ref(false)
  const error = ref(null)

  // Connection configuration
  const dbConfig = {
    remote: 'http://localhost:5984/siadm',
    options: {
      auth: {
        username: process.env.COUCHDB_USER,
        password: process.env.COUCHDB_PASSWORD
      },
      timeout: 10000,
      retry: {
        retries: 3,
        minTimeout: 1000
      }
    }
  }

  // Local instance for synchronization
  const localDB = new PouchDB('siadm_local')
  const remoteDB = new PouchDB(dbConfig.remote, dbConfig.options)

  // Bidirectional synchronization
  async function syncDatabase() {
    try {
      loading.value = true

      const sync = localDB.sync(remoteDB, {
        live: true,
        retry: true,
        filter: 'customers/by_status',
        query_params: { status: 'active' }
      })

      sync.on('change', (info) => {
        console.log('Sync change:', info)
      })

      sync.on('error', (err) => {
        console.error('Sync error:', err)
        error.value = err
      })

      return sync

    } catch (err) {
      error.value = err
      throw err
    } finally {
      loading.value = false
    }
  }

  // Create document with conflict handling
  async function createDocument(doc) {
    try {
      loading.value = true

      // Try local insert first
      const result = await localDB.put(doc)

      // Sync to remote
      await localDB.replicate.to(remoteDB)

      return result

    } catch (err) {
      if (err.status === 409) {
        // Conflict - document already exists
        console.log('Conflict detected, resolving...')
        return await resolveConflict(doc)
      }
      throw err
    } finally {
      loading.value = false
    }
  }

  // Automatic conflict resolution
  async function resolveConflict(doc) {
    const existing = await localDB.get(doc._id)

    // Strategy: keep the most recent
    if (new Date(doc.created_at) > new Date(existing.created_at)) {
      doc._rev = existing._rev
      return await localDB.put(doc)
    }

    return existing
  }

  return {
    localDB,
    remoteDB,
    loading,
    error,
    syncDatabase,
    createDocument
  }
}
Enter fullscreen mode Exit fullscreen mode

** What impressed me:** Offline sync, conflict handling, automatic retry... stuff that would take me DAYS to research!

3. Testing with Quasar Testing Framework

I said: "I need tests for this Vue + CouchDB component"

// test/jest/__tests__/CustomerRegistration.spec.js
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { Quasar } from 'quasar'
import CustomerRegistration from 'src/pages/CustomerRegistration.vue'

// Mock PouchDB
vi.mock('pouchdb', () => ({
  default: vi.fn(() => ({
    find: vi.fn(),
    put: vi.fn(),
    sync: vi.fn()
  }))
}))

describe('CustomerRegistration', () => {
  let wrapper

  beforeEach(() => {
    wrapper = mount(CustomerRegistration, {
      global: {
        plugins: [Quasar]
      }
    })
  })

  it('should render form correctly', () => {
    expect(wrapper.find('[data-test="name-input"]').exists()).toBe(true)
    expect(wrapper.find('[data-test="email-input"]').exists()).toBe(true)
    expect(wrapper.find('[data-test="tax-id-input"]').exists()).toBe(true)
    expect(wrapper.find('[data-test="phone-input"]').exists()).toBe(true)
  })

  it('should validate tax ID correctly', async () => {
    const taxIdInput = wrapper.find('[data-test="tax-id-input"]')

    await taxIdInput.setValue('111.111.111-11') // Invalid tax ID
    await wrapper.find('form').trigger('submit')

    expect(wrapper.text()).toContain('Invalid Tax ID')
  })

  it('should register valid customer', async () => {
    const mockPut = vi.fn().mockResolvedValue({ ok: true, id: 'test-id' })
    wrapper.vm.db.put = mockPut

    await wrapper.find('[data-test="name-input"]').setValue('John Doe')
    await wrapper.find('[data-test="email-input"]').setValue('john@email.com')
    await wrapper.find('[data-test="tax-id-input"]').setValue('123.456.789-09')
    await wrapper.find('[data-test="phone-input"]').setValue('(11) 99999-9999')

    await wrapper.find('form').trigger('submit')
    await wrapper.vm.$nextTick()

    expect(mockPut).toHaveBeenCalledWith(
      expect.objectContaining({
        name: 'John Doe',
        email: 'john@email.com',
        type: 'customer'
      })
    )
  })

  it('should handle duplicate customer error', async () => {
    const mockFind = vi.fn().mockResolvedValue({
      docs: [{ _id: 'existing-customer' }]
    })
    wrapper.vm.db.find = mockFind

    await wrapper.find('[data-test="email-input"]').setValue('john@email.com')
    await wrapper.find('form').trigger('submit')
    await wrapper.vm.$nextTick()

    expect(wrapper.text()).toContain('Customer already registered')
  })
})
Enter fullscreen mode Exit fullscreen mode

Before vs After: The Real Numbers

Development Time

  • Traditional method: 2 weeks (80 hours)
  • With Cursor AI: 4 days (32 hours)
  • Time saved: 60%

Code Quality

  • Initial bugs: 70% fewer
  • Test coverage: Generated automatically
  • Validations: Edge cases I wouldn't think of

Lines of Code

  • Total project: ~1200 lines
  • AI-generated: ~480 lines (40%)
  • My work: Business logic, SIADM-specific configurations

What I Learned (Valuable Lessons)

Where AI Excelled:

  1. Vue Components: Forms, validations, masks
  2. CouchDB Integration: Authentication, sync, conflicts
  3. Quasar Components: Responsive layout, notifications
  4. Composables: Reusable and reactive logic

Where I Still Need the Human:

  1. Business Rules: How SIADM data relationships work
  2. Design System: SiGlobal visual standards
  3. Performance: CouchDB-specific optimizations
  4. Deployment: Quasar production configurations

Best Practices I Discovered:

1. Be specific about your stack:

❌ "Create a form"
✅ "Create a Quasar component with validation that saves to CouchDB"
Enter fullscreen mode Exit fullscreen mode

2. Mention Vue 3 patterns:

"Use Composition API, reactive refs, and async/await"
Enter fullscreen mode Exit fullscreen mode

3. Ask for CouchDB explanations:

"Why use local PouchDB + synchronization?"
Enter fullscreen mode Exit fullscreen mode

Next Steps: What's Coming

Now that I've seen the potential, I'm already planning to use AI for:

  1. SiEscola: Automate grade and attendance reports
  2. SIADM: Intelligent dashboard with insights
  3. PreencheFácil: Real-time frontend validations

Final Thoughts: Did AI Replace the Developer?

Short answer: No.

Long answer: AI became my most efficient pair programming partner. It doesn't make architectural decisions, doesn't understand complex business rules, and doesn't solve unique problems.

But for:

  • Repetitive code ✅
  • Standard validations ✅
  • Basic tests ✅
  • Documentation ✅

It's unbeatable.

Today's developer needs to learn to collaborate with AI, not compete against it.


Final Result

PreencheFácil now integrates seamlessly with SIADM. Customers register through a responsive Quasar interface, data is validated in real-time, and everything syncs automatically with our CouchDB.

Total time: 4 days
Lines of code: 1200+ (40% AI-generated)
Bugs found: Less than 3 (sync worked on first try!)
Happy client: ✅


I work at SiGlobal developing solutions like SIADM, PreencheFácil and SiEscola. I share real development experiences to help other developers.

Need consulting on automation and system integration? Reach out: **ivanrochacardoso@gmail.com**


Comments 0 total

    Add comment