Skip to content

Syllabi Feature Analysis

Backend Implementation

Route Registration

In apps/backend/src/app.ts, two routes are registered for syllabi:

app.route("/syllabi").get(getSyllabi)
app.route("/syllabi/all").get(getAllSyllabi);

Database Interaction

The syllabi.ts controller interacts with what appears to be a Prisma ORM client (db) to query the database.

Endpoint 1: GET /syllabi

Request Processing

  1. Parameter Extraction:
    const numbers = singleToArray(req.query.number).map((n) => n);
    
  2. Converts single course number or array of numbers into a consistent array format

  3. Logging:

    console.log("Fetching syllabi for numbers:", numbers);
    

  4. Logs the requested course numbers for debugging

  5. Database Query:

    const syllabi = await db.syllabi.findMany({
      where: { number: { in: numbers } },
      select: {
        id: true,
        season: true,
        year: true,
        number: true,
        section: true,
        url: true,
      }
    });
    

  6. Uses Prisma's findMany to retrieve matching syllabi
  7. Filters by course numbers using an in clause
  8. Selects only the necessary fields

  9. Error Handling:

    try {
      // Query logic
    } catch (e) {
      console.error("Error fetching syllabi:", e);
      res.json([]);
    }
    

  10. If an error occurs, it's logged
  11. Returns an empty array instead of an error response
  12. This provides graceful degradation for the frontend

  13. Response:

    console.log(`Found ${syllabi.length} syllabi`);
    res.json(syllabi);
    

  14. Logs the number of syllabi found
  15. Returns the syllabi array as JSON

Endpoint 2: GET /syllabi/all

Caching Mechanism

const allSyllabiEntry = {
  allSyllabi: [] as Syllabus[],
  lastCached: new Date(1970),
};
- Simple in-memory cache with timestamp

Cache Invalidation Logic

if (new Date().valueOf() - allSyllabiEntry.lastCached.valueOf() > 1000 * 60 * 60 * 24) {
  // Cache is expired (older than 24 hours)
}

Data Retrieval

const syllabiFromDB = await db.syllabi.findMany({
  where: filter,
  select: getAllSyllabiDbQuery.select,
});

const syllabi: Syllabus[] = syllabiFromDB.map((s) => ({
  id: s.id,
  season: s.season ?? "",
  year: s.year ?? 0,
  number: s.number ?? "",
  url: s.url ?? null,
}));
- Fetches all syllabi from database - Maps database objects to the Syllabus type - Handles potential null values with nullish coalescing

Frontend Implementation

API Client (apps/frontend/src/app/api/syllabi.ts)

Core Data Fetching Function

const fetchSyllabi = async (numbers: string[]): Promise<Syllabus[]> => {
  try {
    const url = `${process.env.NEXT_PUBLIC_BACKEND_URL || ""}/syllabi`;

    console.log("Original numbers (with types):", numbers.map(n => `${n} (${typeof n})`));

    // Ensure numbers are always strings and padded to 5 digits
    const paddedNumbers = numbers.map(num => String(num).padStart(5, '0'));

    console.log("Padded numbers:", paddedNumbers);

    const params = new URLSearchParams(
      paddedNumbers.map((number) => ["number", number])
    );

    const response = await axios.get(url, {
      headers: {"Content-Type": "application/json"},
      params,
    });

    return response.data;
  } catch (error) {
    console.error("Error fetching syllabi:", error);
    return [];
  }
}
- Ensures course numbers are consistently formatted (padded to 5 digits) - Uses axios for HTTP requests - Extensive logging for debugging - Returns empty array on error (matching backend behavior)

React Query Integration

Single Course Hook

export const useFetchSyllabus = (number: string) => {
  return useQuery({
    queryKey: ["syllabus", { number }],
    queryFn: () => fetchSyllabusBatcher.fetch(number),
    staleTime: STALE_TIME,
    enabled: !!number,
  });
};
- Uses React Query for data fetching, caching, and state management - Only enabled when a valid course number is provided - Uses the batching mechanism for efficient API calls

Multiple Courses Hook

export const useFetchMultipleSyllabi = (numbers: string[]) => {
  return useQueries({
    queries: numbers.map((number) => ({
      queryKey: ["syllabus", { number }],
      queryFn: () => fetchSyllabusBatcher.fetch(number),
      staleTime: STALE_TIME,
      enabled: !!number,
    })),
    combine: (result) => {
      return result.reduce((acc, { data }) => {
        if (data) acc.push(data);
        return acc;
      }, [] as Syllabus[]);
    },
  });
};
- Handles multiple course syllabi in parallel - Combines individual query results into a single array - Still benefits from the batching mechanism

Batching Mechanism

const fetchSyllabusBatcher = create({
  fetcher: async (syllabusNumbers: string[]): Promise<Syllabus[]> => {
    try {
      const url = `${process.env.NEXT_PUBLIC_BACKEND_URL || ""}/syllabi`;
      const params = new URLSearchParams(
        syllabusNumbers.map((number) => ["number", number])
      );

      const response = await axios.get(url, {
        headers: {
          "Content-Type": "application/json",
        },
        params,
      });

      return response.data;
    } catch (error) {
      console.error("Error in syllabus batcher:", error);
      return []; 
    }
  },
  resolver: keyResolver("number"),
  scheduler: windowScheduler(10),
});
- Uses the batshit library to efficiently batch requests - Groups multiple requests within a 10ms window - Reduces network overhead for multiple syllabus requests - Uses a resolver to match returned syllabi to their course numbers

Complete Data Flow (End-to-End)

  1. User Interaction:
  2. A user views a course that has an associated syllabus
  3. Or a user searches for specific course syllabi

  4. Component Rendering:

  5. A React component calls useFetchSyllabus("15112") or similar

  6. React Query Processing:

  7. React Query checks its cache for ["syllabus", {number: "15112"}]
  8. If found and not stale (< 24 hours old), returns cached data
  9. If not found or stale, proceeds with the request

  10. Request Batching:

  11. The request enters the batching queue
  12. If other requests arrive within 10ms window, they're batched together
  13. This creates a single request for multiple course numbers

  14. HTTP Request:

  15. Axios sends a GET request to /syllabi?number=15112
  16. Or for batched requests: /syllabi?number=15112&number=15122&number=15150

  17. Backend Processing:

  18. Express server receives the request
  19. Routes it to the getSyllabi handler
  20. Extracts course numbers from query params

  21. Database Query:

  22. Backend queries the database for matching syllabi
  23. Selects only necessary fields (id, season, year, number, section, url)

  24. Response Generation:

  25. Backend formats the database results
  26. Returns JSON array of matching syllabi

  27. Frontend Processing:

  28. Axios receives the HTTP response
  29. React Query stores the data in cache (valid for 24 hours)
  30. Component receives the data and renders accordingly

  31. User Experience:

    • User sees syllabus information for the course(s)
    • Potentially clicks on the syllabus URL to view the full document

This comprehensive flow demonstrates how the syllabi feature efficiently retrieves and displays syllabi information through a well-designed data pipeline.