@instructure/platform-canvas-fetch
v0.1.1
Published
Canvas-aware fetch wrapper with TypeScript and Zod validation support.
Readme
@instructure/platform-canvas-fetch
Canvas-aware fetch wrapper with TypeScript and Zod validation support.
Features
- Automatic CSRF token handling from cookies
- Smart body handling (JSON, FormData, strings)
- Mandatory Zod schema validation
- Automatic Link header parsing for pagination
- Throws on HTTP errors (4xx/5xx)
- TypeScript-first with full type inference
- Canvas API conventions (query params, headers)
Installation
pnpm add @instructure/platform-canvas-fetch zodBasic Usage
import { canvasFetch } from '@instructure/platform-canvas-fetch'
import { z } from 'zod'
// Define your response schema
const SubmissionSchema = z.object({
id: z.string(),
user_id: z.string(),
assignment_id: z.string(),
sticker: z.string().nullable()
})
// Make a request
const { data, response, link } = await canvasFetch(
{
path: '/api/v1/courses/123/assignments/456/submissions/789',
method: 'GET'
},
SubmissionSchema
)
// data is fully typed and validated
console.log(data.sticker) // string | nullAPI
canvasFetch(options, schema)
Main fetch function with Zod validation.
Options:
path(string, required) - API endpoint pathmethod('GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE') - HTTP method (default: 'GET')body(object | FormData | string) - Request bodyparams(object) - Query parameters (encoded with Canvas conventions)headers(object) - Additional headers to merge with defaultssignal(AbortSignal) - For request cancellation
Returns: Promise<CanvasFetchResult<T>>
data- Validated response data (typed from Zod schema)response- Raw Response objectlink- Parsed Link headers (for pagination)
Throws: CanvasFetchError on HTTP errors or validation failures
configureCanvasFetch(config)
Configure global options.
import { configureCanvasFetch } from '@instructure/platform-canvas-fetch'
configureCanvasFetch({
baseUrl: 'https://canvas.instructure.com'
})Examples
POST Request with Body
const NewSubmissionSchema = z.object({
id: z.string(),
workflow_state: z.string()
})
const { data } = await canvasFetch(
{
path: '/api/v1/courses/123/assignments/456/submissions',
method: 'POST',
body: {
submission: {
submission_type: 'online_text_entry',
body: 'My submission'
}
}
},
NewSubmissionSchema
)Query Parameters (Canvas-style)
const { data } = await canvasFetch(
{
path: '/api/v1/courses/123/assignments',
params: {
per_page: 50,
include: ['submission', 'score_statistics'], // Encoded as include[]=...
order_by: 'due_at'
}
},
AssignmentsSchema
)
// Request: /api/v1/courses/123/assignments?per_page=50&include[]=submission&include[]=score_statistics&order_by=due_atPagination with TanStack Query
import { useInfiniteQuery } from '@tanstack/react-query'
const AssignmentSchema = z.object({
id: z.string(),
name: z.string()
})
const AssignmentsSchema = z.array(AssignmentSchema)
function useAssignments(courseId: string) {
return useInfiniteQuery({
queryKey: ['assignments', courseId],
queryFn: async ({ pageParam }) => {
return canvasFetch(
{
path: pageParam || `/api/v1/courses/${courseId}/assignments`,
params: { per_page: 50 }
},
AssignmentsSchema
)
},
getNextPageParam: (lastPage) => lastPage.link?.next?.url,
initialPageParam: undefined
})
}Error Handling
import { CanvasFetchError } from '@instructure/platform-canvas-fetch'
try {
const { data } = await canvasFetch({ path: '/api/v1/...' }, Schema)
} catch (error) {
if (error instanceof CanvasFetchError) {
console.error('HTTP Status:', error.status)
console.error('Response:', error.response)
console.error('Error Body:', error.json)
}
}Custom Headers
const { data } = await canvasFetch(
{
path: '/api/v1/courses',
headers: {
'Accept': 'application/json+canvas-string-ids', // Opt-in to string IDs
'X-Custom-Header': 'value'
}
},
CoursesSchema
)Request Cancellation
const controller = new AbortController()
const request = canvasFetch(
{
path: '/api/v1/courses',
signal: controller.signal
},
CoursesSchema
)
// Cancel the request
controller.abort()Default Headers
Automatically included on every request:
X-Requested-With: XMLHttpRequestX-CSRF-Token: [from _csrf_token cookie]Accept: application/jsoncredentials: same-origin
Security Considerations
Error Response Data
When CanvasFetchError is thrown, the error.json property contains the raw response body from the server. Do not render this data directly to the DOM without proper sanitization, as it may contain user-generated content or malicious payloads that could lead to XSS vulnerabilities.
Safe:
try {
await canvasFetch({ path: '/api/v1/...' }, Schema)
} catch (error) {
if (error instanceof CanvasFetchError) {
// Log for debugging - safe
console.error('API Error:', error.json)
// Display generic message - safe
showErrorToast('An error occurred. Please try again.')
}
}Unsafe:
try {
await canvasFetch({ path: '/api/v1/...' }, Schema)
} catch (error) {
if (error instanceof CanvasFetchError) {
// ❌ DANGEROUS - could inject malicious HTML/scripts
element.innerHTML = error.json.message
}
}Always sanitize user-facing error messages or use a framework's built-in XSS protection (e.g., React's JSX automatically escapes).
