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 apiKey points:
withCredentials: trueensures thesis_sessionHttpOnly cookie is sent with every request- The CSRF token is read from the
sis_csrfnon-HttpOnly cookie and added asX-CSRF-Tokenheader on mutating requests (POST, PUT, PATCH, DELETE) - The
Accept-Languageheader is set for server-side i18n - A separate
authAxiosinstance 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:
| File | Domain |
|---|---|
academic.api.ts | Courses, degrees, majors, terms, sections |
academicPolicy.api.ts | Institutional academic policy rules (/audit/policy-rules) |
audit.api.ts | Degree auditing, waivers, transfer credits |
auditConfig.api.ts | Audit-trail table configuration |
auditTrail.api.ts | Audit-trail search |
auth.api.ts | Login, logout, user management |
authorization.api.ts | Roles, permissions, dashboard tiles |
billing.api.ts | Tuition, fees, payment plans |
communications.api.ts | Staff-to-student email: recipients, senders, outbox |
creditConversions.api.ts | Credit conversion rules |
demoAccounts.api.ts | Demo-instance sign-in accounts |
emailTemplates.api.ts | Email template management |
filterPresets.api.ts | Saved list filters |
formbuilder.api.ts | Dynamic form templates |
grading.api.ts | GPA, transcripts, grade rosters |
holds.api.ts | Student holds |
lmsintegration.api.ts | LMS connectors, instances, shells, exports |
lookups.api.ts | Lookup table values |
organization.api.ts | Campuses, buildings, departments |
people.api.ts | Persons, names, addresses |
permissionTransfer.api.ts | Permission transfer between users |
planning.api.ts | Registration planning |
proxy.api.ts | Parent/guardian access |
registration.api.ts | Enrollments, waitlists |
reporting.api.ts | IPEDS, reports |
roles.api.ts | Students, teachers, advisors |
selfprofile.api.ts | The signed-in person's own profile (/people/me) |
sequencePolicy.api.ts | Course sequencing policies |
settings.api.ts | Application settings |
sso.api.ts | SAML identity providers |
student.api.ts | Student-specific endpoints |
testscores.api.ts | Test scores and policies |
transaction.api.ts | Registration transactions |
uiText.api.ts | UI Text overrides |
workflow.api.ts | Registration workflows |
workflowEngine.api.ts | Generic 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.