Skip to content

API Services ​

Backend communication patterns.

Axios Instance ​

The Axios instance is configured for cookie-based authentication with CSRF protection:

typescript
// api/index.ts
import axios from 'axios'
import i18n from '../i18n'

// API base URL -- relative path, proxied through Vite in dev
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v1'

/**
 * Read the CSRF token from the non-HttpOnly cookie (sis_csrf).
 */
function getCookie(name: string): string | null {
  const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
  return match?.[2] ? decodeURIComponent(match[2]) : null
}

const api = axios.create({
  baseURL: API_BASE_URL,
  timeout: 30000,
  withCredentials: true, // Send cookies with every request
  headers: {
    'Content-Type': 'application/json',
  }
})

// Request interceptor -- add CSRF token and Accept-Language header
api.interceptors.request.use((config) => {
  // Send current locale to backend for i18n
  config.headers['Accept-Language'] = i18n.global.locale.value

  // Add CSRF token for mutating requests
  const method = config.method?.toUpperCase()
  if (method && !['GET', 'HEAD', 'OPTIONS'].includes(method)) {
    const csrfToken = getCookie('sis_csrf')
    if (csrfToken) {
      config.headers['X-CSRF-Token'] = csrfToken
    }
  }
  return config
})

// Response interceptor -- handle session expiry
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      // Session expired -- logout and redirect to login
      const authStore = useAuthStore()
      await authStore.logout()
      router.push({ name: 'login', query: { expired: 'true' } })
    }
    return Promise.reject(error)
  }
)

export default api

Key points:

  • withCredentials: true ensures the sis_session HttpOnly cookie is sent with every request
  • The CSRF token is read from the sis_csrf non-HttpOnly cookie and added as X-CSRF-Token header on mutating requests (POST, PUT, PATCH, DELETE)
  • The Accept-Language header is set for server-side i18n
  • A separate authAxios instance is used for auth endpoints to avoid infinite redirect loops on 401

Service Pattern ​

Each domain has its own API service file in src/api/services/:

typescript
// api/services/academic.api.ts
import api from '../index'
import type {
  Course,
  CourseCreate,
  PaginatedResponse
} from '../types'

export const courseApi = {
  getAll: (params?: Record<string, any>) =>
    api.get<PaginatedResponse<Course>>('/academic/courses', { params }),

  getById: (id: number) =>
    api.get<Course>(`/academic/courses/${id}`),

  create: (data: CourseCreate) =>
    api.post<Course>('/academic/courses', data),

  update: (id: number, data: Partial<CourseCreate>) =>
    api.put<Course>(`/academic/courses/${id}`, data),

  delete: (id: number) =>
    api.delete(`/academic/courses/${id}`),

  // Custom action
  activate: (id: number) =>
    api.post(`/academic/courses/${id}/activate`)
}

There are 36 API service files covering all backend domains:

FileDomain
academic.api.tsCourses, degrees, majors, terms, sections
academicPolicy.api.tsInstitutional academic policy rules (/audit/policy-rules)
audit.api.tsDegree auditing, waivers, transfer credits
auditConfig.api.tsAudit-trail table configuration
auditTrail.api.tsAudit-trail search
auth.api.tsLogin, logout, user management
authorization.api.tsRoles, permissions, dashboard tiles
billing.api.tsTuition, fees, payment plans
communications.api.tsStaff-to-student email: recipients, senders, outbox
creditConversions.api.tsCredit conversion rules
demoAccounts.api.tsDemo-instance sign-in accounts
emailTemplates.api.tsEmail template management
filterPresets.api.tsSaved list filters
formbuilder.api.tsDynamic form templates
grading.api.tsGPA, transcripts, grade rosters
holds.api.tsStudent holds
lmsintegration.api.tsLMS connectors, instances, shells, exports
lookups.api.tsLookup table values
organization.api.tsCampuses, buildings, departments
people.api.tsPersons, names, addresses
permissionTransfer.api.tsPermission transfer between users
planning.api.tsRegistration planning
proxy.api.tsParent/guardian access
registration.api.tsEnrollments, waitlists
reporting.api.tsIPEDS, reports
roles.api.tsStudents, teachers, advisors
selfprofile.api.tsThe signed-in person's own profile (/people/me)
sequencePolicy.api.tsCourse sequencing policies
settings.api.tsApplication settings
sso.api.tsSAML identity providers
student.api.tsStudent-specific endpoints
testscores.api.tsTest scores and policies
transaction.api.tsRegistration transactions
uiText.api.tsUI Text overrides
workflow.api.tsRegistration workflows
workflowEngine.api.tsGeneric approval workflows: definitions, instances, approvals

TypeScript Types ​

typescript
// api/types/index.ts

// Pagination wrapper
export interface PaginatedResponse<T> {
  count: number
  next: string | null
  previous: string | null
  results: T[]
}

// Entity types
export interface Course {
  course_id: number
  course_code: string
  title: string
  description: string | null
  credit_hours_min: number
  credit_hours_max: number
  department_id: number
  department_name: string
  status_code: string
  status_display: string
  created_at: string
  updated_at: string
}

export interface CourseCreate {
  course_code: string
  title: string
  description?: string
  credit_hours_min: number
  credit_hours_max: number
  department_id: number
  status_id: number
}

// Auth types
export interface User {
  id: number
  email: string
  person_id: string
  is_staff: boolean
  is_superuser: boolean
  is_student: boolean
  is_faculty: boolean
  is_advisor: boolean
  roles: string[]
}

export interface LoginCredentials {
  username: string
  password: string
}

Usage in Components ​

vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import type { Course } from '@/api/types'
import { courseApi } from '@/api/services/academic.api'

const { t } = useI18n()
const courses = ref<Course[]>([])
const loading = ref(false)

async function loadCourses() {
  loading.value = true
  try {
    const response = await courseApi.getAll({
      department_id: 1,
      ordering: 'course_code'
    })
    courses.value = response.data.results
  } finally {
    loading.value = false
  }
}

onMounted(() => loadCourses())
</script>

Error Handling ​

typescript
import { handleApiError } from '@/api/index'
import { useToast } from 'primevue/usetoast'

const toast = useToast()

async function saveCourse(data: CourseCreate) {
  try {
    await courseApi.create(data)
    toast.add({
      severity: 'success',
      summary: 'Success',
      detail: 'Course created',
      life: 3000
    })
  } catch (error) {
    const message = handleApiError(error)
    toast.add({
      severity: 'error',
      summary: 'Error',
      detail: message,
      life: 5000
    })
  }
}

The handleApiError helper in api/index.ts extracts user-friendly error messages from API responses and falls back to i18n-translated generic messages for common HTTP status codes.

Student Information System Documentation