Skip to content

Response Interceptors

Response interceptors allow you to handle HTTP responses from form submissions based on their status codes. They provide a powerful way to customize behavior after create or update operations, such as showing custom notifications, redirecting users, updating application state, or handling errors.

Overview

Response interceptors are functions triggered after form submission based on the HTTP status code of the response. Each interceptor receives:

  • formId: The unique identifier of the form
  • response: The complete HTTP response object

FancyCRUD provides:

  • Per-Form Interceptors: Define specific behavior for individual forms
  • Global Interceptors: Set default behavior for all forms
  • Built-in Error Handling: Automatic error notifications for 4xx and 5xx status codes
  • Status Code Targeting: Handle any HTTP status code (200, 201, 400, 404, 500, etc.)

Basic Usage

Define interceptors directly in your form configuration:

vue
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
import { useRouter } from 'vue-router'

const router = useRouter()

const form = useForm({
  fields: {
    name: {
      type: FieldType.text,
      label: 'Name'
    },
    email: {
      type: FieldType.text,
      label: 'Email'
    }
  },
  settings: {
    url: 'users/'
  },
  responseInterceptor: {
    200: (formId, response) => {
      console.log('Update successful:', response.data)
      // Handle successful update
    },
    201: (formId, response) => {
      console.log('Created successfully:', response.data)
      // Redirect to the new resource
      router.push(`/users/${response.data.id}`)
    },
    400: (formId, response) => {
      console.error('Bad request:', response.data)
      // Handle validation errors from server
    }
  }
})
</script>

Common Status Codes

Here are the most common HTTP status codes you'll encounter:

Success Responses (2xx)

CodeMeaningCommon Use Case
200OKSuccessful update (PATCH/PUT)
201CreatedSuccessful creation (POST)
204No ContentSuccessful deletion

Client Errors (4xx)

CodeMeaningCommon Use Case
400Bad RequestValidation errors, malformed data
401UnauthorizedAuthentication required
403ForbiddenInsufficient permissions
404Not FoundResource doesn't exist
409ConflictDuplicate resource, constraint violation
422Unprocessable EntitySemantic validation errors

Server Errors (5xx)

CodeMeaningCommon Use Case
500Internal Server ErrorServer-side error
502Bad GatewayUpstream server error
503Service UnavailableServer temporarily down

Built-in Error Handling

FancyCRUD automatically displays error notifications for status codes 400-451 and 500-511. You can override this behavior by providing custom interceptors for these codes.

Use Cases & Examples

1. Success Notifications

Show custom success messages based on the operation:

vue
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
import { useToast } from 'vue-toastification'

const toast = useToast()

const form = useForm({
  fields: {
    title: { type: FieldType.text, label: 'Title' }
  },
  settings: {
    url: 'posts/'
  },
  responseInterceptor: {
    200: (formId, response) => {
      toast.success('Post updated successfully!')
    },
    201: (formId, response) => {
      toast.success(`Post "${response.data.title}" created successfully!`)
    }
  }
})
</script>

2. Redirect After Success

Navigate to different pages based on the response:

vue
<script lang="ts" setup>
import { useForm, FieldType, FORM_MODE } from '@fancy-crud/vue'
import { useRouter } from 'vue-router'

const router = useRouter()

const form = useForm({
  fields: {
    name: { type: FieldType.text, label: 'Name' }
  },
  settings: {
    url: 'products/',
    mode: FORM_MODE.create
  },
  responseInterceptor: {
    201: (formId, response) => {
      // Redirect to the newly created product
      router.push(`/products/${response.data.id}`)
    },
    200: (formId, response) => {
      // Redirect to product list after update
      router.push('/products')
    }
  }
})
</script>

3. Handle Validation Errors

Display server-side validation errors:

vue
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
import { useToast } from 'vue-toastification'

const toast = useToast()

const form = useForm({
  fields: {
    email: { type: FieldType.text, label: 'Email' },
    username: { type: FieldType.text, label: 'Username' }
  },
  settings: {
    url: 'users/'
  },
  responseInterceptor: {
    400: (formId, response) => {
      // Display server validation errors
      const errors = response.data.errors
      if (errors) {
        Object.entries(errors).forEach(([field, messages]) => {
          toast.error(`${field}: ${messages.join(', ')}`)
        })
      }
    },
    422: (formId, response) => {
      // Handle unprocessable entity errors
      toast.error(response.data.message || 'Validation failed')
    }
  }
})
</script>

4. Handle Authentication Errors

Redirect to login on authentication failures:

vue
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const router = useRouter()
const authStore = useAuthStore()

const form = useForm({
  fields: {
    content: { type: FieldType.textarea, label: 'Content' }
  },
  settings: {
    url: 'posts/'
  },
  responseInterceptor: {
    401: (formId, response) => {
      // Clear auth and redirect to login
      authStore.logout()
      router.push('/login')
    },
    403: (formId, response) => {
      // Show permission error
      alert('You do not have permission to perform this action')
    }
  }
})
</script>

5. Update Application State

Sync application state with server response:

vue
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

const form = useForm({
  fields: {
    name: { type: FieldType.text, label: 'Name' },
    bio: { type: FieldType.textarea, label: 'Bio' }
  },
  settings: {
    url: 'profile/'
  },
  responseInterceptor: {
    200: (formId, response) => {
      // Update user store with new data
      userStore.updateProfile(response.data)
    }
  }
})
</script>

6. Handle Conflicts

Deal with duplicate resources or race conditions:

vue
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
import { useToast } from 'vue-toastification'

const toast = useToast()

const form = useForm({
  fields: {
    email: { type: FieldType.text, label: 'Email' }
  },
  settings: {
    url: 'users/'
  },
  responseInterceptor: {
    409: (formId, response) => {
      // Handle duplicate email
      toast.warning('This email is already registered')
      // Optionally suggest alternative actions
    }
  }
})
</script>

7. Retry Logic for Server Errors

Implement retry mechanism for temporary failures:

vue
<script lang="ts" setup>
import { ref } from 'vue'
import { useForm, FieldType } from '@fancy-crud/vue'

const retryCount = ref(0)
const maxRetries = 3

const form = useForm({
  fields: {
    data: { type: FieldType.text, label: 'Data' }
  },
  settings: {
    url: 'submissions/'
  },
  responseInterceptor: {
    500: (formId, response) => {
      if (retryCount.value < maxRetries) {
        retryCount.value++
        console.log(`Retrying... (${retryCount.value}/${maxRetries})`)
        setTimeout(() => {
          form.submit()
        }, 1000 * retryCount.value) // Exponential backoff
      } else {
        alert('Failed after multiple attempts. Please try again later.')
        retryCount.value = 0
      }
    },
    200: () => {
      retryCount.value = 0 // Reset on success
    },
    201: () => {
      retryCount.value = 0 // Reset on success
    }
  }
})
</script>

Global Response Interceptors

Define default interceptors for all forms in your application by configuring them in your FancyCRUD configuration file:

ts
import { defineConfig } from '@fancy-crud/vue'
import router from '@/router'
import { useToast } from 'vue-toastification'

const toast = useToast()

export const fancyCrud = defineConfig({
  // Other configurations...

  responseInterceptors: {
    // Success: Show success notification
    200: (formId, response) => {
      toast.success('Updated successfully')
    },
    
    // Created: Show success and log
    201: (formId, response) => {
      toast.success('Created successfully')
      console.log('New resource:', response.data)
    },
    
    // Bad Request: Log validation errors
    400: (formId, response) => {
      console.error('Validation errors:', response.data)
    },
    
    // Unauthorized: Redirect to login
    401: (formId, response) => {
      toast.error('Session expired. Please log in again.')
      router.push('/login')
    },
    
    // Forbidden: Show permission error
    403: (formId, response) => {
      toast.error('You do not have permission to perform this action')
    },
    
    // Not Found: Handle missing resources
    404: (formId, response) => {
      toast.error('Resource not found')
    },
    
    // Server Error: Show generic error
    500: (formId, response) => {
      toast.error('Server error. Please try again later.')
    }
  }
})

Combining Global and Local Interceptors

Local interceptors override global ones for the same status code:

vue
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'

const form = useForm({
  fields: {
    name: { type: FieldType.text, label: 'Name' }
  },
  settings: {
    url: 'users/'
  },
  responseInterceptor: {
    // This overrides the global 201 interceptor for this form
    201: (formId, response) => {
      console.log('Custom handling for user creation')
      // Different behavior than global interceptor
    }
    // All other status codes will use global interceptors
  }
})
</script>

Interceptor Priority

Local interceptors take precedence over global interceptors. If you define both, only the local interceptor will execute for that status code.

Accessing Form and Response Data

Interceptors receive both the formId and the full response object:

vue
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
import { IFormStore } from '@fancy-crud/core'

const formStore = inject(IFormStore)

const form = useForm({
  fields: {
    title: { type: FieldType.text, label: 'Title' }
  },
  settings: {
    url: 'posts/'
  },
  responseInterceptor: {
    201: (formId, response) => {
      // Access the form instance
      const formInstance = formStore.searchById(formId)
      
      // Access response data
      console.log('Status:', response.status)
      console.log('Headers:', response.headers)
      console.log('Data:', response.data)
      
      // Access form fields
      console.log('Form title:', formInstance.fields.title.modelValue)
      
      // Reset form after successful creation
      formInstance.reset()
    }
  }
})
</script>

Complete Example

Here's a comprehensive example demonstrating advanced response interceptor usage:

vue
<template>
  <div class="card">
    <f-form v-bind="form" />
    
    <!-- Show server errors -->
    <div v-if="serverErrors.length" class="error-list">
      <h4>Server Errors:</h4>
      <ul>
        <li v-for="error in serverErrors" :key="error">{{ error }}</li>
      </ul>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { ref, computed } from 'vue'
import { useForm, FieldType, FORM_MODE } from '@fancy-crud/vue'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { useProductStore } from '@/stores/products'

const router = useRouter()
const toast = useToast()
const productStore = useProductStore()

const serverErrors = ref<string[]>([])
const retryCount = ref(0)

const form = useForm({
  fields: {
    name: {
      type: FieldType.text,
      label: 'Product Name',
      required: true
    },
    price: {
      type: FieldType.text,
      label: 'Price',
      required: true
    },
    category: {
      type: FieldType.select,
      label: 'Category',
      options: ['Electronics', 'Clothing', 'Food'],
      required: true
    },
    description: {
      type: FieldType.textarea,
      label: 'Description'
    }
  },
  settings: {
    url: 'products/',
    mode: FORM_MODE.create,
    title: '{{ Create Product | Edit Product }}'
  },
  responseInterceptor: {
    // Success: Created
    201: (formId, response) => {
      toast.success(`Product "${response.data.name}" created successfully!`)
      
      // Add to store
      productStore.addProduct(response.data)
      
      // Clear any errors
      serverErrors.value = []
      
      // Redirect to product details
      router.push(`/products/${response.data.id}`)
    },
    
    // Success: Updated
    200: (formId, response) => {
      toast.success('Product updated successfully!')
      
      // Update store
      productStore.updateProduct(response.data)
      
      // Clear errors
      serverErrors.value = []
      
      // Redirect to product list
      router.push('/products')
    },
    
    // Validation Error
    400: (formId, response) => {
      serverErrors.value = []
      
      // Handle field-specific errors
      if (response.data.errors) {
        Object.entries(response.data.errors).forEach(([field, messages]: [string, any]) => {
          if (Array.isArray(messages)) {
            messages.forEach(msg => serverErrors.value.push(`${field}: ${msg}`))
          }
        })
      }
      
      // Show generic message
      if (response.data.message) {
        toast.error(response.data.message)
      }
    },
    
    // Unauthorized
    401: (formId, response) => {
      toast.error('Your session has expired. Please log in again.')
      router.push('/login')
    },
    
    // Forbidden
    403: (formId, response) => {
      toast.error('You do not have permission to manage products')
      router.push('/dashboard')
    },
    
    // Not Found (for updates)
    404: (formId, response) => {
      toast.error('Product not found. It may have been deleted.')
      router.push('/products')
    },
    
    // Conflict (duplicate)
    409: (formId, response) => {
      toast.warning('A product with this name already exists')
      serverErrors.value = ['Product name must be unique']
    },
    
    // Unprocessable Entity
    422: (formId, response) => {
      toast.error('Invalid product data')
      serverErrors.value = [response.data.message || 'Validation failed']
    },
    
    // Server Error with Retry
    500: (formId, response) => {
      if (retryCount.value < 3) {
        retryCount.value++
        toast.info(`Server error. Retrying... (${retryCount.value}/3)`)
        
        setTimeout(() => {
          form.submit()
        }, 1000 * retryCount.value)
      } else {
        toast.error('Server error. Please try again later.')
        serverErrors.value = ['Server is temporarily unavailable']
        retryCount.value = 0
      }
    },
    
    // Service Unavailable
    503: (formId, response) => {
      toast.error('Service is temporarily down for maintenance')
    }
  }
})

// Switch to edit mode
const editProduct = (productId: string) => {
  form.settings.mode = FORM_MODE.update
  form.settings.lookupValue = productId
  serverErrors.value = []
}

// Switch to create mode
const createNewProduct = () => {
  form.settings.mode = FORM_MODE.create
  form.settings.lookupValue = null
  form.reset()
  serverErrors.value = []
}
</script>

<style scoped>
.error-list {
  margin-top: 1rem;
  padding: 1rem;
  background-color: #fee;
  border-left: 4px solid #f00;
  border-radius: 4px;
}

.error-list h4 {
  margin: 0 0 0.5rem 0;
  color: #c00;
}

.error-list ul {
  margin: 0;
  padding-left: 1.5rem;
}

.error-list li {
  color: #c00;
  margin-bottom: 0.25rem;
}
</style>

Best Practices

Response Interceptor Tips

  1. Handle Common Errors Globally: Define global interceptors for common status codes (401, 403, 500)
  2. Use TypeScript: Type your response data for better IDE support
  3. Keep It Simple: Don't put complex business logic in interceptors
  4. Log for Debugging: Console log responses during development
  5. Provide User Feedback: Always show toast notifications or messages
  6. Reset Form State: Clear errors and reset retry counters on success
  7. Graceful Degradation: Handle unexpected status codes gracefully

Common Pitfalls

  • Don't modify form state excessively: Keep interceptors focused on response handling
  • Avoid async operations: Interceptors should be synchronous; use callbacks for async work
  • Don't ignore errors: Always handle error status codes appropriately
  • Test all paths: Ensure interceptors work for all expected status codes

Response Object Structure

The response object typically contains:

ts
{
  status: number,        // HTTP status code
  statusText: string,    // Status message
  data: any,            // Response body
  headers: object,      // Response headers
  config: object        // Request configuration
}

TypeScript Support

Define typed interceptors for better type safety:

typescript
import type { AxiosResponse } from 'axios'

interface UserResponse {
  id: string
  name: string
  email: string
  created_at: string
}

interface ErrorResponse {
  message: string
  errors?: Record<string, string[]>
}

const form = useForm({
  fields: {
    name: { type: FieldType.text, label: 'Name' }
  },
  settings: {
    url: 'users/'
  },
  responseInterceptor: {
    201: (formId: symbol, response: AxiosResponse<UserResponse>) => {
      // response.data is typed as UserResponse
      console.log('User created:', response.data.name)
    },
    400: (formId: symbol, response: AxiosResponse<ErrorResponse>) => {
      // response.data is typed as ErrorResponse
      console.error('Validation failed:', response.data.message)
    }
  }
})

Next Steps