Integrate Bolt.new with Moodle by enabling Moodle's Web Services, creating an external service and token in Site Administration, then calling Moodle's function-based REST API from Next.js API routes using the wsfunction parameter pattern. Moodle's API differs from standard REST — every call specifies a function name like core_course_get_courses. Store your token in .env and never expose it client-side.
Building Custom Learning Dashboards with Moodle's Web Services API
Moodle is different from every other API you will encounter in Bolt.new development. Rather than resource-based URLs (GET /courses, POST /enrollments), Moodle exposes all its functionality through a single endpoint that accepts a function name as a parameter. Want courses? Call wsfunction=core_course_get_courses. Want assignments? Call wsfunction=mod_assign_get_assignments. This design, inherited from Moodle's PHP architecture, means every integration follows the same pattern regardless of what data you are accessing — a single fetch to the same URL, varying only the function name and parameters.
This architecture has a major practical benefit for Bolt developers: once you understand the pattern, adding new Moodle capabilities to your app is fast. The Moodle Web Services documentation lists over 200 available functions covering every aspect of the LMS — courses, users, grades, assignments, quizzes, forums, and messaging. You can build a custom student portal, a faculty gradebook, a corporate training tracker, or an executive dashboard showing completion rates, all from the same API foundation.
The setup requires Moodle administrator access. A Moodle developer (with Site Administration access) needs to enable Web Services, create an external service listing the specific functions your app needs, create a user account for the integration, and generate a token. Once you have the token and your Moodle site's URL, the integration is straightforward: API calls go through Next.js API routes to keep the token server-side, the Moodle endpoint returns JSON, and your React components display the data. Moodle's Web Services work over standard HTTPS, which is fully compatible with Bolt's WebContainer for outbound calls.
Integration method
Bolt generates the Moodle integration code — API routes that call Moodle's Web Services REST endpoint — through conversation with the AI. Unlike modern REST APIs, Moodle uses a function-based model where every call specifies a wsfunction parameter (like core_course_get_courses) against a single endpoint. Your Moodle administrator must enable Web Services, create an external service with the required functions, and generate a token. All API calls go through Next.js server-side routes to keep the token out of the browser.
Prerequisites
- Moodle Site Administration access (you need admin credentials to enable Web Services — student/teacher accounts cannot do this)
- A Moodle instance accessible over HTTPS (self-hosted or Moodle.com — version 3.9 or later recommended)
- Web Services enabled in Moodle: Site Administration → Server → Web Services → Overview → Enable web services
- An external service created in Moodle with the specific wsfunction names your app will call
- A Moodle Web Services token generated for your integration user (Site Administration → Server → Web Services → Manage tokens)
Step-by-step guide
Enable Moodle Web Services and generate an API token
Enable Moodle Web Services and generate an API token
Before writing any code, your Moodle instance must have Web Services enabled and configured. This requires Site Administration access — you cannot set this up as a regular user. Log into your Moodle site as an administrator and navigate through the steps carefully. Go to Site Administration → Server → Web Services → Overview. This page shows a checklist of required setup steps. Work through them in order: Enable Web Services (toggle it on), Enable Protocols (enable the REST protocol), and Create a service (create a new External Service that lists exactly which API functions your app will use — only add the functions you need, not all functions). For a basic student dashboard, add these functions to your external service: core_course_get_courses, core_enrol_get_users_courses, mod_assign_get_assignments, gradereport_user_get_grade_items, core_user_get_users_by_field. For a more complete integration, add: core_completion_get_activities_completion_status, mod_assign_get_submissions, core_enrol_get_enrolled_users. Next, create or designate a Moodle user account for your integration (ideally a dedicated service account, not an admin account). Assign this user the 'webservice' role or ensure it has the capabilities required by the functions you added. Then go to Site Administration → Server → Web Services → Manage Tokens, click Add, select your service and user, and generate a token. Copy this token — it is a long string and will be your primary authentication credential. Store the token in your Bolt project's .env.local file. Also store your Moodle site URL. These are the only two configuration values needed to call the API.
Create a Next.js app that integrates with a Moodle LMS Web Services API. Create a utility file lib/moodle.ts that exports a moodleCall function. This function takes a wsfunction name and parameters object, calls the Moodle REST endpoint at {MOODLE_URL}/webservice/rest/server.php, passes the wstoken, moodlewsrestformat=json, and the wsfunction and other parameters as query parameters for GET requests. Handle Moodle's exception response format — if the response has an 'exception' field, throw an error with the message field. Store MOODLE_URL and MOODLE_TOKEN in .env.
Paste this in Bolt.new chat
1// .env.local2MOODLE_URL=https://your-moodle-site.com3MOODLE_TOKEN=your_web_services_token_here45// lib/moodle.ts6const MOODLE_URL = process.env.MOODLE_URL;7const MOODLE_TOKEN = process.env.MOODLE_TOKEN;89interface MoodleException {10 exception: string;11 errorcode: string;12 message: string;13 debuginfo?: string;14}1516export async function moodleCall<T>(17 wsfunction: string,18 params: Record<string, string | number | boolean> = {}19): Promise<T> {20 if (!MOODLE_URL || !MOODLE_TOKEN) {21 throw new Error('MOODLE_URL and MOODLE_TOKEN must be set in environment variables');22 }2324 const url = new URL(`${MOODLE_URL}/webservice/rest/server.php`);25 url.searchParams.set('wstoken', MOODLE_TOKEN);26 url.searchParams.set('moodlewsrestformat', 'json');27 url.searchParams.set('wsfunction', wsfunction);2829 Object.entries(params).forEach(([key, value]) => {30 url.searchParams.set(key, String(value));31 });3233 const response = await fetch(url.toString(), {34 headers: { 'Content-Type': 'application/json' },35 });3637 if (!response.ok) {38 throw new Error(`Moodle API HTTP error: ${response.status}`);39 }4041 const data = await response.json() as T | MoodleException;4243 // Moodle returns exceptions as JSON with an 'exception' field44 if (data && typeof data === 'object' && 'exception' in data) {45 const err = data as MoodleException;46 throw new Error(`Moodle API error: ${err.message} (${err.errorcode})`);47 }4849 return data as T;50}Pro tip: Moodle's Web Services REST endpoint always uses GET for reading data, even for operations that feel like POST (such as grade submissions — those use POST with form-encoded body). The moodleCall utility above handles GET requests. For write operations, add a separate POST-capable variant that sends parameters in the request body.
Expected result: Your .env.local has MOODLE_URL and MOODLE_TOKEN set. The moodleCall utility is ready to use in API routes. Testing with a simple call like core_site_get_site_info returns your Moodle site name and version, confirming the token is valid.
Fetch courses and enrollments from the Moodle API
Fetch courses and enrollments from the Moodle API
With the moodleCall utility in place, you can now create Next.js API routes that retrieve course and enrollment data. These routes act as a proxy between your React frontend and Moodle — they receive requests from the browser, call Moodle using the server-side token, and return normalized JSON to the client. The most common starting point is fetching a user's enrolled courses. The function core_enrol_get_users_courses returns the courses that a specific Moodle user is enrolled in, along with progress, last access time, and course metadata. It requires the user's Moodle ID as a parameter — not a username or email. To look up a user's Moodle ID by email, use core_user_get_users_by_field first. For a course listing dashboard, you will typically want: the course ID, full name, short name, course image URL, category, start and end dates, progress percentage, and last access timestamp. The core_enrol_get_users_courses function returns most of these in its response. Map the raw Moodle response to a clean interface that your React components expect — this normalization layer makes your frontend code much cleaner and protects it from Moodle API changes. Note on Moodle's response format: arrays of items come back as JSON arrays, but individual items with fields like course metadata come back as objects. Some functions return a nested structure with course data inside a courses key, while others return the array directly. Check the Moodle Web Services documentation for each function to understand its specific response shape.
Create API routes for Moodle data. First, create /api/moodle/user/[email]/route.ts that calls core_user_get_users_by_field to look up a Moodle user ID by email address. Second, create /api/moodle/courses/[userId]/route.ts that calls core_enrol_get_users_courses with the user ID and returns a normalized array with id, fullname, shortname, progress, lastaccess (formatted date), imageUrl, and category. Use the moodleCall utility from lib/moodle.ts.
Paste this in Bolt.new chat
1// app/api/moodle/courses/[userId]/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { moodleCall } from '@/lib/moodle';45interface MoodleCourse {6 id: number;7 fullname: string;8 shortname: string;9 overviewfiles?: Array<{ fileurl: string }>;10 progress?: number;11 lastaccess?: number;12 categoryname?: string;13 startdate?: number;14 enddate?: number;15 visible?: number;16}1718export async function GET(19 request: NextRequest,20 { params }: { params: { userId: string } }21) {22 const userId = parseInt(params.userId, 10);2324 if (isNaN(userId)) {25 return NextResponse.json({ error: 'Invalid user ID' }, { status: 400 });26 }2728 try {29 const courses = await moodleCall<MoodleCourse[]>(30 'core_enrol_get_users_courses',31 { userid: userId }32 );3334 const normalized = courses.map((course) => ({35 id: course.id,36 fullname: course.fullname,37 shortname: course.shortname,38 progress: course.progress ?? 0,39 lastAccess: course.lastaccess40 ? new Date(course.lastaccess * 1000).toISOString()41 : null,42 imageUrl: course.overviewfiles?.[0]?.fileurl43 ? `${course.overviewfiles[0].fileurl}?token=${process.env.MOODLE_TOKEN}`44 : null,45 category: course.categoryname ?? 'Uncategorized',46 startDate: course.startdate ? new Date(course.startdate * 1000).toISOString() : null,47 endDate: course.enddate ? new Date(course.enddate * 1000).toISOString() : null,48 isVisible: course.visible === 1,49 }));5051 return NextResponse.json({ courses: normalized, total: normalized.length });52 } catch (error) {53 const message = error instanceof Error ? error.message : 'Unknown error';54 return NextResponse.json({ error: message }, { status: 500 });55 }56}Pro tip: Moodle stores timestamps as Unix timestamps (seconds since epoch), not milliseconds. Multiply by 1000 before passing to JavaScript's Date constructor. Also, Moodle course images require the API token appended as a query parameter to be accessible — this is handled in the imageUrl mapping above.
Expected result: Calling /api/moodle/courses/123 (with a valid Moodle user ID) returns a JSON array of courses that user is enrolled in, with progress percentages and last access dates.
Fetch assignments and grades for student progress tracking
Fetch assignments and grades for student progress tracking
Beyond courses, the most valuable data for a student dashboard is assignment deadlines and current grades. Moodle exposes these through separate Web Service functions — mod_assign_get_assignments for assignment data and gradereport_user_get_grade_items for the grade report. The mod_assign_get_assignments function returns all assignments across one or more course IDs. Pass course IDs as an array using Moodle's indexed parameter convention: courseids[0]=101&courseids[1]=102. Each assignment in the response includes the name, due date (Unix timestamp), cut-off date, description, and submission status information. For grades, gradereport_user_get_grade_items returns the complete gradebook for a user in a specific course — similar to what you see in the Moodle gradebook view. The response includes each grade item's name, grade value, maximum grade, feedback, and percentage. This is the most useful endpoint for showing a student their overall standing in each course. Be aware of a performance consideration: fetching assignments across many courses in a single call can be slow if the Moodle server is under load. For a student enrolled in 10+ courses, consider fetching course data first, then loading assignments on-demand when a user clicks into a specific course, rather than loading all assignments upfront. This lazy loading pattern keeps the initial dashboard fast. Also note that Moodle's Web Services do not push real-time updates — your app must poll for new data. For a dashboard refreshed on page load, this is fine. For real-time grade updates, Moodle does not provide a native webhook or event stream — the standard approach is polling on a schedule.
Create two more Moodle API routes. First: /api/moodle/assignments/[courseId]/route.ts that calls mod_assign_get_assignments with courseids[0]={courseId} and returns assignments with id, name, dueDate (ISO string), cutoffDate, description, and submissionsOpen (boolean based on whether duedate is in the future). Second: /api/moodle/grades/[courseId]/[userId]/route.ts that calls gradereport_user_get_grade_items and returns grade items with itemName, grade, gradeMax, percentage, and feedback.
Paste this in Bolt.new chat
1// app/api/moodle/assignments/[courseId]/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { moodleCall } from '@/lib/moodle';45interface MoodleAssignment {6 id: number;7 cmid: number;8 name: string;9 duedate: number;10 cutoffdate: number;11 intro: string;12 nosubmissions: number;13}1415interface AssignmentsResponse {16 courses: Array<{17 id: number;18 assignments: MoodleAssignment[];19 }>;20}2122export async function GET(23 request: NextRequest,24 { params }: { params: { courseId: string } }25) {26 const courseId = parseInt(params.courseId, 10);2728 if (isNaN(courseId)) {29 return NextResponse.json({ error: 'Invalid course ID' }, { status: 400 });30 }3132 try {33 const data = await moodleCall<AssignmentsResponse>(34 'mod_assign_get_assignments',35 { 'courseids[0]': courseId }36 );3738 const now = Date.now();39 const assignments = (data.courses[0]?.assignments ?? []).map((a) => ({40 id: a.id,41 name: a.name,42 dueDate: a.duedate ? new Date(a.duedate * 1000).toISOString() : null,43 cutoffDate: a.cutoffdate ? new Date(a.cutoffdate * 1000).toISOString() : null,44 description: a.intro.replace(/<[^>]*>/g, ''), // Strip HTML from intro45 submissionsOpen: a.nosubmissions === 0 && (a.duedate === 0 || a.duedate * 1000 > now),46 isOverdue: a.duedate > 0 && a.duedate * 1000 < now,47 }));4849 return NextResponse.json({50 courseId,51 assignments,52 total: assignments.length,53 });54 } catch (error) {55 const message = error instanceof Error ? error.message : 'Unknown error';56 return NextResponse.json({ error: message }, { status: 500 });57 }58}Pro tip: Moodle assignment descriptions contain HTML markup (it is a rich text field in Moodle). Strip HTML tags before displaying in your React components, or use dangerouslySetInnerHTML with DOMPurify sanitization for formatted display. Never render unsanitized Moodle HTML directly.
Expected result: Calling /api/moodle/assignments/456 returns a list of assignments for course 456 with due dates formatted as ISO strings and an isOverdue flag for assignments past their deadline.
Build a student dashboard and handle Moodle errors
Build a student dashboard and handle Moodle errors
With the API routes in place, build the React dashboard UI. The student dashboard should show enrolled courses as cards with progress indicators, upcoming assignment deadlines sorted by due date, and current grades for selected courses. This gives students everything they need without navigating Moodle's complex default interface. The main dashboard page should prompt the user for their Moodle user ID (or derive it from an authenticated session if you have one) and fetch their courses on load. Each course card shows the course name, progress percentage, last access time, and a click to expand assignments and grades for that course. For the assignment list, filter to show only future assignments (duedate in the future) by default, with an option to show past assignments. Sort by due date ascending so the most urgent assignments appear first. Highlight assignments due within 48 hours in yellow, overdue in red. Remember that Bolt's WebContainer handles outbound API calls fine — your dashboard can call /api/moodle/courses and /api/moodle/assignments during development without deploying. The only Moodle integration feature that requires deployment would be any webhook or scheduled sync, and Moodle's Web Services do not natively support outgoing webhooks (Moodle has an Events API for plugins, but not a standard HTTP webhook for external apps). For real-time data in a production deployment, implement polling with SWR or React Query.
Build a MoodleDashboard React component. It should prompt for a Moodle user ID, fetch their courses from /api/moodle/courses/[userId], and display them as cards in a responsive grid. Each card shows the course name, progress bar (0-100%), last access date, and category. Clicking a course card expands it to show assignments fetched from /api/moodle/assignments/[courseId] sorted by due date. Highlight overdue assignments in red and due-within-48-hours in yellow. Show a loading skeleton while fetching. Add error handling for invalid user IDs.
Paste this in Bolt.new chat
1// components/MoodleDashboard.tsx2'use client';34import { useState } from 'react';56interface Course {7 id: number;8 fullname: string;9 shortname: string;10 progress: number;11 lastAccess: string | null;12 category: string;13 imageUrl: string | null;14}1516interface Assignment {17 id: number;18 name: string;19 dueDate: string | null;20 isOverdue: boolean;21 submissionsOpen: boolean;22 description: string;23}2425function urgencyClass(assignment: Assignment): string {26 if (assignment.isOverdue) return 'border-l-4 border-red-500 bg-red-50';27 if (assignment.dueDate) {28 const hoursLeft = (new Date(assignment.dueDate).getTime() - Date.now()) / 3600000;29 if (hoursLeft < 48) return 'border-l-4 border-yellow-400 bg-yellow-50';30 }31 return 'border-l-4 border-gray-200';32}3334export default function MoodleDashboard() {35 const [userId, setUserId] = useState('');36 const [courses, setCourses] = useState<Course[]>([]);37 const [assignments, setAssignments] = useState<Record<number, Assignment[]>>({});38 const [loading, setLoading] = useState(false);39 const [error, setError] = useState('');40 const [expandedCourse, setExpandedCourse] = useState<number | null>(null);4142 async function fetchCourses() {43 if (!userId) return;44 setLoading(true);45 setError('');46 try {47 const res = await fetch(`/api/moodle/courses/${userId}`);48 const data = await res.json();49 if (data.error) throw new Error(data.error);50 setCourses(data.courses);51 } catch (e) {52 setError(e instanceof Error ? e.message : 'Failed to load courses');53 } finally {54 setLoading(false);55 }56 }5758 async function loadAssignments(courseId: number) {59 if (assignments[courseId]) {60 setExpandedCourse(expandedCourse === courseId ? null : courseId);61 return;62 }63 const res = await fetch(`/api/moodle/assignments/${courseId}`);64 const data = await res.json();65 setAssignments((prev) => ({ ...prev, [courseId]: data.assignments ?? [] }));66 setExpandedCourse(courseId);67 }6869 return (70 <div className="p-6 max-w-4xl mx-auto">71 <h1 className="text-2xl font-bold mb-6">Moodle Student Dashboard</h1>72 <div className="flex gap-3 mb-8">73 <input74 type="number"75 value={userId}76 onChange={(e) => setUserId(e.target.value)}77 placeholder="Moodle User ID"78 className="border rounded px-3 py-2 w-48"79 />80 <button81 onClick={fetchCourses}82 disabled={loading}83 className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"84 >85 {loading ? 'Loading...' : 'Load Courses'}86 </button>87 </div>88 {error && <p className="text-red-600 mb-4">{error}</p>}89 <div className="grid gap-4">90 {courses.map((course) => (91 <div key={course.id} className="border rounded-lg overflow-hidden">92 <button93 onClick={() => loadAssignments(course.id)}94 className="w-full p-4 text-left hover:bg-gray-50"95 >96 <div className="flex justify-between items-start">97 <div>98 <h2 className="font-semibold">{course.fullname}</h2>99 <p className="text-sm text-gray-500">{course.category}</p>100 </div>101 <span className="text-sm text-blue-600">{course.progress}% complete</span>102 </div>103 <div className="mt-2 h-2 bg-gray-200 rounded">104 <div105 className="h-2 bg-blue-500 rounded"106 style={{ width: `${course.progress}%` }}107 />108 </div>109 </button>110 {expandedCourse === course.id && assignments[course.id] && (111 <div className="border-t p-4 bg-gray-50">112 <h3 className="font-medium mb-3">Assignments</h3>113 {assignments[course.id].length === 0 ? (114 <p className="text-sm text-gray-500">No assignments.</p>115 ) : (116 assignments[course.id].map((a) => (117 <div key={a.id} className={`mb-2 p-3 rounded ${urgencyClass(a)}`}>118 <p className="font-medium text-sm">{a.name}</p>119 <p className="text-xs text-gray-500">120 {a.dueDate121 ? `Due: ${new Date(a.dueDate).toLocaleDateString()}`122 : 'No due date'}123 {a.isOverdue && ' — OVERDUE'}124 </p>125 </div>126 ))127 )}128 </div>129 )}130 </div>131 ))}132 </div>133 </div>134 );135}Pro tip: For production deployments, consider implementing a simple caching layer (using Next.js revalidation or an in-memory cache) for course data. Moodle course enrollment does not change frequently, so caching for 5-10 minutes significantly reduces load on your Moodle server.
Expected result: Entering a Moodle user ID and clicking Load Courses shows that user's enrolled courses as expandable cards. Clicking a course reveals its assignments, with overdue items highlighted in red and upcoming-deadline items in yellow.
Common use cases
Custom student progress dashboard
Build a simplified student-facing portal that shows enrolled courses, assignment due dates, and current grades in a cleaner interface than Moodle's default theme. Students see their academic status at a glance without navigating Moodle's complex menu structure.
Create a Next.js app connected to a Moodle LMS. Build a student dashboard that fetches enrolled courses for a specific user ID using core_enrol_get_users_courses, shows assignment due dates using mod_assign_get_assignments, and displays current grades. Use the Moodle Web Services REST API with a token from .env. Show courses as cards with progress indicators.
Copy this prompt to try it in Bolt.new
Corporate training completion tracker
Build an internal HR dashboard showing which employees have completed mandatory training courses in Moodle. HR managers can filter by department, see completion percentages, and identify employees who have not started required courses.
Build a training completion dashboard that calls Moodle's Web Services API. Fetch all users enrolled in a specific course using core_enrol_get_enrolled_users, get their activity completion data using core_completion_get_activities_completion_status, and display a table showing each employee's name, department, completion percentage, and last access date. Add a filter for incomplete-only view.
Copy this prompt to try it in Bolt.new
Faculty grade submission tool
Create a streamlined grading interface for instructors that pulls assignment submissions from Moodle, lets faculty enter grades in a simple form, and submits them back to Moodle via the API — all without navigating Moodle's complex grading workflows.
Create a faculty grading interface that fetches assignment submissions from Moodle using mod_assign_get_submissions and mod_assign_get_submission_statuses. Display each student's submission with a text input for the grade (0-100). On save, call mod_assign_save_grade to submit grades back to Moodle. Add bulk grade entry and a preview before submitting.
Copy this prompt to try it in Bolt.new
Troubleshooting
Moodle API returns an exception object with errorcode 'invalidtoken' instead of data
Cause: The MOODLE_TOKEN environment variable is incorrect, the token was revoked in Moodle's Web Services management, or the token belongs to a different Moodle site than MOODLE_URL points to.
Solution: Log into your Moodle site as an administrator and go to Site Administration → Server → Web Services → Manage Tokens to confirm the token is active and not expired. Copy the token again carefully — it is a 32-character alphanumeric string with no spaces. Set it in your .env.local file and restart the Next.js dev server. If the token is missing from the Manage Tokens page, generate a new one.
1// Test token validity with the simplest possible function:2const result = await moodleCall('core_webservice_get_site_info');3console.log('Site:', result.sitename, 'Moodle version:', result.release);API returns 'accessexception' or 'nopermission' error for specific wsfunction calls
Cause: The function is not added to the External Service that your token belongs to, or the Moodle user account the token represents does not have the capability required to call that function.
Solution: In Moodle Site Administration → Server → Web Services → External Services, open your service and check that the specific wsfunction is listed under 'Functions'. If it is missing, add it. Also check that the user assigned to the token has the required Moodle role capabilities — teacher-level functions require at minimum a teacher role in the relevant courses.
fetch to Moodle URL fails with CORS error in the browser console during development
Cause: The Moodle API call is being made from client-side React code directly, not through a Next.js API route. Moodle does not send CORS headers that allow browser-origin requests.
Solution: Move all Moodle API calls to server-side Next.js API routes in the app/api/ directory. Client components should fetch from your own API routes (e.g., /api/moodle/courses/123), not directly from the Moodle URL. Server-side fetch calls are not subject to CORS restrictions.
1// WRONG — direct Moodle call from client component:2const data = await fetch('https://moodle.school.edu/webservice/rest/server.php?...');34// CORRECT — call your own API route from client:5const data = await fetch('/api/moodle/courses/123');Course images do not load — broken image icons appear instead of course thumbnails
Cause: Moodle course overview images require the Web Services token appended as a query parameter to authenticate the file request. Without the token, Moodle returns a 403 Unauthorized response.
Solution: Append ?token={MOODLE_TOKEN} to the fileurl from overviewfiles. This is handled in the API route above. However, if you are passing the imageUrl directly to an <img> src in the browser, the token is visible in the page HTML. For security, proxy image requests through your own API route that fetches and streams the image without exposing the token to the client.
1// Safe approach: proxy image through your API route2// app/api/moodle/image/route.ts — fetch and stream the Moodle image server-side3export async function GET(request: NextRequest) {4 const { searchParams } = new URL(request.url);5 const fileUrl = searchParams.get('url');6 const response = await fetch(`${fileUrl}?token=${process.env.MOODLE_TOKEN}`);7 return new NextResponse(response.body, {8 headers: { 'Content-Type': response.headers.get('Content-Type') ?? 'image/jpeg' },9 });10}Best practices
- Create a dedicated Moodle service account for your integration rather than using an admin account — this limits what the API token can access and makes it easier to audit API usage.
- Only add the specific wsfunction names your app needs to the External Service, not all available functions. This is a security principle of least privilege — if the token is compromised, attackers can only call the functions you explicitly enabled.
- Never call Moodle's Web Services endpoint directly from client-side React. Always proxy through Next.js API routes to keep your token server-side and avoid CORS errors.
- Strip HTML from Moodle text fields (course descriptions, assignment intros) before displaying them — Moodle stores these as rich HTML. Use a simple regex for display text or DOMPurify for formatted display.
- Cache course and enrollment data that does not change frequently. A 5-minute cache significantly reduces load on your Moodle server for dashboards that many students access simultaneously.
- Always deploy to Netlify or Bolt Cloud before testing in a real school environment — while Moodle API calls work in the WebContainer preview, real student data should only flow through a secured, deployed application.
- Handle Moodle's Unix timestamp format (seconds, not milliseconds) by multiplying by 1000 before passing to JavaScript Date. Missing this conversion produces dates in 1970.
Alternatives
Canvas uses standard RESTful API design (resource URLs, HTTP verbs) which is far more intuitive than Moodle's function-based approach, making it easier to integrate for developers new to LMS APIs.
Google Classroom uses the familiar Google API OAuth pattern and is a better fit if your users are already in the Google Workspace ecosystem and do not need the full LMS feature set.
Blackboard is the dominant LMS in large US universities and offers a modern REST API, making it the right integration target when your users are primarily at American higher education institutions.
Frequently asked questions
Do I need to self-host Moodle to use the Web Services API with Bolt.new?
No. Both self-hosted Moodle instances and Moodle.com (the managed cloud service) support Web Services. You just need administrator access to enable Web Services and create a token. Contact your Moodle administrator if you do not have Site Administration access.
Can I test the Moodle integration in Bolt's WebContainer preview?
Yes. Moodle API calls from your Next.js API routes are outbound HTTP requests, which work fine in Bolt's WebContainer. You can fully test fetching courses, assignments, and grades in the preview without deploying. The only thing that requires deployment is any feature that needs to receive incoming HTTP requests, which Moodle's standard Web Services do not use.
How do I look up a Moodle user ID from an email address?
Use the core_user_get_users_by_field function with field=email and values[0]=user@example.com. This returns the user object including their Moodle numeric ID. In a production app, look up and cache the Moodle user ID during authentication so you do not need to call this endpoint on every page load.
Is there a way to write data back to Moodle — for example, submitting grades from my Bolt app?
Yes. Functions like mod_assign_save_grade and gradereport_grader_get_grades allow writing grades back to Moodle. These functions use POST requests with parameters in the request body. The user account associated with your token must have grading permissions in the relevant course — a teacher or grader role. Write operations should be used carefully and always validated before submission.
What Moodle version do I need for Web Services to work?
Moodle Web Services have been available since Moodle 2.0 (2010). The functions used in this guide (core_course_get_courses, mod_assign_get_assignments, gradereport_user_get_grade_items) are stable and available in all Moodle versions from 3.x onwards. Moodle 3.9 LTS and Moodle 4.x are recommended for current integrations.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation