Request for Community Review đź‘€ on ADRs for standardizing the Open edX platform API endpoints

Hi folks,
We are currently working on ADRs for standardizing the Open edX platform API endpoints. And We’d like to request feedback from the community as well.

  1. ADR for standardizing serializer usage (related issue: Link & related PRs: PR1, PR2) - Merged.
  2. ADR for standardizing permission classes usage (related issue: Link & related PR: PR1) - In Review.
  3. ADR for standardizing api documentation schema usage (related issue: Link & related PR: PR1) - In Review.
  4. ADR for standardizing restful api endpoints usage (related issues: Link1, Link2 & related PR: PR1) - In Review.
  5. ADR for merging similar endpoints (related issue Link & related PR: PR1) - In Review.
  6. ADR for standardizing error responses (related issue Link & related PR: PR1) - In Review.
  7. ADR for ensuring GET requests are idempotent (related issue Link & related PR: PR1) - In Review.

We have also started development on first approved ADR related to serializer standardization. For now the PR (Link) is in draft state. In this PR, we have targeted EnrollmentView and EnrollmentAllowedView views to use DRF serializer for input validation and output formatting.

Feel free to add your thoughts, thanks.

I asked Claude to generate a summary of these PRs, and here’s the result:


Below is a high-level summary of the seven ADRs currently up for review, along with key decisions and points worth discussing. I hope this is useful as a companion to the individual PRs.

Overview

All seven ADRs represent a coordinated effort to eliminate inconsistency across the Open edX platform’s REST API layer as part of FC-0118. The common thread: many endpoints were built ad hoc over the years, and they now diverge in how they handle serialization, authorization, documentation, URL structure, error formatting, and HTTP semantics. These ADRs propose a set of standards to bring them into alignment.

Two ADRs have already merged (0025 and 0026). Five are open for review.


ADR-by-ADR Summary

ADR 0025 — Standardize Serializer Usage (PR #38139, PR #38188) :white_check_mark: Merged

Problem: Many endpoints manually construct JSON responses using Python dicts instead of DRF serializers, leading to inconsistent schemas and making automated tooling (including AI integrations) unreliable.

Decision: All API views must use explicit DRF serializers for both request validation and response formatting. Manual JSON construction is to be replaced.

Key points:

  • Serializers must include field descriptions and validation rules.
  • A follow-up amendment (PR #38188) clarified that if full backward compatibility isn’t achievable, a new API version must be created and the old one deprecated — not silently broken.
  • Alternatives like Pydantic and dataclasses were considered and rejected to avoid introducing a third pattern alongside existing DRF serializers and manual dicts.

Possible discussion points:

  • How do we handle ModelSerializer-heavy endpoints where field exposure is difficult to predict?
  • What’s the migration priority order — are there particularly high-traffic or externally-consumed endpoints that should go first?

ADR 0026 — Standardize Permission Classes (PR #38187) :white_check_mark: Merged

Problem: Authorization logic is scattered across custom decorators, inline role checks, and embedded logic within views — creating security gaps and making access patterns difficult to audit.

Decision: All API views must declare authorization via permission_classes on the DRF view. Inline role checks and custom decorators should be replaced with reusable BasePermission subclasses.

Key points:

  • This ADR standardizes the DRF integration surface for authorization, not the underlying policy engine. Existing legacy checks or future engines (e.g. Casbin) can be wrapped inside permission classes during phased migrations.
  • Object-level permissions (has_object_permission) are explicitly encouraged.

Possible discussion points:

  • What’s the recommended pattern for views that currently mix IsAuthenticated with legacy Django permission checks? Should there be shared base classes provided by the working group?
  • How should permission classes interact with API key authentication (e.g. ApiKeyPermissionMixIn)?

ADR 0027 — Standardize API Documentation & Schema Coverage (PR #38189) :magnifying_glass_tilted_left: Open for Review

Problem: Many views lack OpenAPI schema decorators, making endpoints undiscoverable for external developers, AI agents, and integration tooling. Duplicate endpoints have also emerged partly due to poor discoverability.

Decision: All endpoints must use drf-spectacular with @extend_schema decorators, covering request/response schemas, status codes, and error conditions.

Key points:

  • Documentation must include descriptions and examples for complex endpoints.
  • The ADR explicitly calls out AI-tool discoverability as a motivation alongside standard developer experience goals.

Possible discussion points:

  • Should there be a linting or CI gate that fails builds when views lack @extend_schema?
  • drf-spectacular is already used in parts of the platform — are there any incompatibilities or known gaps with existing auto-generated schemas?

ADR 0028 — Standardize RESTful Endpoints Using DRF ViewSets (PR #38191) :magnifying_glass_tilted_left: Open for Review

Problem: Related API actions (list, retrieve, create, update, delete) are often spread across separate APIView classes with duplicated logic, making the codebase hard to maintain and extend.

Decision: Related actions must be consolidated into DRF ViewSets registered via DRF Routers.

Key points:

  • get_queryset must use select_related/prefetch_related to prevent N+1 query regressions during migrations.
  • Multi-method handler functions (e.g. a single method dispatching DELETE, POST, and PUT) must be broken into action-specific ViewSet methods.
  • Backward compatibility must be maintained; breaking changes require API versioning + deprecation.
  • The Enrollment API (three separate APIView classes for one resource) and the Assets handling endpoint are called out as specific migration targets.

Possible discussion points:

  • ViewSets impose a fairly specific URL shape via DRF Routers — are there cases in the platform where existing URL patterns can’t easily conform to this without breaking external clients?
  • How should custom actions (@action decorator) be documented in the standard? Some of the existing fragmented endpoints are conceptually “actions on a resource” rather than standard CRUD.
  • This ADR and ADR 0031 (“Merge Similar Endpoints”) are closely related — should they be read as a sequence, or could they conflict on some endpoints?

ADR 0029 — Standardize Error Responses (PR #38246) :magnifying_glass_tilted_left: Open for Review

Problem: Open edX APIs return errors in multiple incompatible shapes — {"error": ...}, {"detail": ...}, nested field dicts, and even HTTP 200 with "success": false. This makes reliable error handling impossible for clients.

Decision: All non-2xx responses must use a structured JSON error object loosely based on RFC 9457 (Problem Details).

Key points:

  • Core fields: type (URI identifier), title, status, detail, instance (request path).
  • Optional user_message field for MFE/end-user display; MFEs should prefer mapping type URIs to locally-translated strings.
  • Validation errors use a predictable errors dict (field → list of messages), mapping directly onto DRF’s native ValidationError.detail.
  • A catalog of common type URIs is proposed: https://openedx.org/errors/not-found, authz, authn, validation, rate-limited, internal. App-specific types may extend this but must use absolute URIs.
  • The ADR explicitly prohibits masking errors behind HTTP 200.

Possible discussion points:

  • The openedx.org/errors/* URIs need to actually resolve to documentation eventually — who owns that and what’s the timeline?
  • How should the central DRF exception handler be shipped and adopted across repos? Will it live in edx-drf-extensions or openedx-platform?
  • The user_message field is optional. Should there be guidance on when it’s appropriate vs. relying on type-based client-side translation?

ADR 0030 — Ensure GET Requests Are Idempotent (PR #38249) :magnifying_glass_tilted_left: Open for Review

Problem: Some GET endpoints fire openedx-events, Django signals, or trigger writes as side-effects. This violates REST safety semantics, breaks caching proxies, and creates invisible domain-state mutations.

Decision: GET handlers must be strictly read-only with respect to domain state. Side-effecting operations must be moved to explicit write endpoints (POST/PUT/PATCH).

Key points:

  • Firing openedx-events or Django signals from GET handlers is explicitly prohibited — this is called the “primary concern” because signal receivers can cause downstream domain writes invisible to the GET handler.
  • Pure telemetry writes to a separate analytics store (e.g. tracker.emit, Segment) are acceptable inside GET if they don’t affect domain state and don’t involve openedx-events/signals.
  • Regression tests must be added to ensure GET handlers remain side-effect free.
  • Specific call-outs: discussion views, legacy courseware code, and student app code that emit analytics in GET paths.

Possible discussion points:

  • The telemetry carve-out (pure analytics writes are OK) may be unclear in practice — when does an analytics write risk affecting domain state? Could this exception be tightened?
  • How should teams handle endpoints where the GET side-effect is intentional and relied upon by downstream systems? Is the migration path just “add a POST endpoint and deprecate the side-effect”?
  • Are there automated detection strategies (AST analysis, test tooling) being considered to find non-idempotent GETs across the codebase?

ADR 0031 — Merge Similar Endpoints (PR #38262) :magnifying_glass_tilted_left: Open for Review

Problem: The platform has grown clusters of near-identical endpoints for the same resource domain (e.g. three separate certificate endpoints: enable_certificate_generation, start_certificate_generation, start_certificate_regeneration), each independently duplicating permission checks, serialization logic, and audit logging.

Decision: Groups of closely related endpoints should be consolidated into a single parameterized DRF view, using an action or mode request parameter to distinguish operations.

Key points:

  • Shared logic (permissions, validation, audit logging) should move into a common service layer or mixin.
  • URL aliases or deprecation redirects must preserve backward compatibility during the transition window.
  • The unified endpoint schema must be fully documented via drf-spectacular, including the enumerated set of valid action/mode values.

Possible discussion points:

  • Using an action parameter to dispatch to different operations is a departure from pure REST conventions — is there a preference for using HTTP verbs semantically vs. a parameter-based dispatch approach?
  • How does this ADR interact with ADR 0028 (ViewSets)? The ViewSet pattern via DRF Routers already groups related actions — is the action/mode parameter pattern intended for cases where standard CRUD verbs don’t map cleanly?
  • Certificate generation as a motivating example involves Celery background tasks. Should the ADR address async patterns (e.g. returning a task ID + polling endpoint) as part of consolidation?

Cross-Cutting Themes

A few themes run across multiple ADRs that might be worth discussing holistically:

Backward compatibility and versioning — ADRs 0025, 0026, 0028, and 0031 all touch this. The consistent answer is: “maintain compatibility; if you can’t, version the API.” It would be worth agreeing on a canonical deprecation timeline and whether there’s tooling (e.g. a deprecation header or registry) to help track it.

AI/machine discoverability — ADRs 0025, 0027, and 0029 all explicitly mention AI tools as a beneficiary of these changes. This is a significant framing shift for API design guidance and worth acknowledging as a stated goal.

Migration sequencing — These seven ADRs are interdependent (e.g. ViewSets depend on Serializers; Documentation depends on Serializers and ViewSets; Error responses work best with proper HTTP status codes from all layers). Is there a recommended adoption order, or will teams apply them incrementally?

Enforcement — None of the ADRs currently specify CI enforcement. Community input on which of these could or should be lint/tested in CI would strengthen the overall proposal.


Summary prepared based on the ADR content in PRs #38139, #38188, #38187, #38189, #38191, #38246, #38249, and #38262. Please refer to the original PRs for full detail and normative text.

1 Like

Community Review: Open edX REST API Standardization ADRs — Batch 2 Summary & Discussion Points

Following on from the first batch, six more ADRs have been opened. Below is a high-level summary of each, along with key decisions and points worth discussing. All six are currently open for review.


ADR-by-ADR Summary

ADR 0032 — Standardize Pagination Across APIs (PR #38300) :magnifying_glass_tilted_left: Open for Review

Problem: List endpoints use at least three different pagination approaches — page/page_size, limit/offset, or no pagination at all. Every API consumer must implement custom data-loading logic per endpoint, and unbounded result sets risk overloading clients.

Decision: All list-type endpoints must use the DefaultPagination class from edx-drf-extensions (a subclass of DRF’s PageNumberPagination, defaulting to page size 10, max 100).

Key points:

  • Endpoints using LimitOffsetPagination must migrate to DefaultPagination with appropriate API versioning.
  • All paginated responses must include a standard envelope: count, next, previous, num_pages, current_page, start, results.
  • Views using APIView directly (rather than GenericAPIView/ListAPIView) must manually invoke the pagination API.
  • Custom page size overrides per endpoint are acceptable when justified (e.g. smaller default for mobile), but must subclass DefaultPagination rather than use an unrelated class.
  • The Course Blocks API (/api/courses/v2/blocks/) is explicitly noted as a difficult case — it intentionally returns unpaginated full course structure trees, and may need special handling.

Possible discussion points:

  • The Course Blocks API and similar tree-structure responses may deserve different treatment.
  • The DefaultPagination default of page size 10 with a max of 100 may be too small for some existing consumers — is there a migration strategy for clients that currently rely on getting all results in one call?
  • Should the standard envelope fields (especially num_pages, start) be explicitly blessed by aligning with any existing platform conventions, to avoid confusion with count-only patterns some clients already parse?

ADR 0033 — Standardize Filtering/Sorting Parameters (PR #38303) :magnifying_glass_tilted_left: Open for Review

Problem: Filtering and sorting syntax varies across APIs — inconsistent parameter names (e.g. course_id vs course), different filter semantics, and no predictable ordering convention. Clients must hardcode endpoint-specific logic and automated tooling can’t reliably infer query patterns.

Decision: Adopt django-filter for list endpoints requiring filtering; standardize on a single ordering parameter (DRF convention); normalize parameter names (e.g. consistently use course_id); expose all filters and ordering options via OpenAPI schema.

Key points:

  • django-filter is already used in several parts of the platform (user API, experiments, entitlements) — this ADR standardizes and extends existing usage rather than introducing something new.
  • The ADR includes a three-phase backward-compatibility strategy: support old and new parameter names in parallel → deprecation warnings via HTTP headers → removal after two release cycles.
  • Deprecated parameters must be marked deprecated: true in OpenAPI schemas.
  • Note: this ADR has Status: Accepted rather than Proposed, unlike the others in this batch.

Possible discussion points:

  • The two-release-cycle removal window: is that sufficient for operators and MFEs that may not be closely tracking API changes?
  • Are there standard HTTP headers already used for deprecation warnings in the platform, or does this need to be defined alongside ADR 0029 (error responses)?

ADR 0034 — Standardize Authentication Patterns (PR #38301) :magnifying_glass_tilted_left: Open for Review

Problem: Open edX APIs use OAuth2, JWT, and session authentication inconsistently — sometimes mixing multiple mechanisms in a single endpoint. External integrators can’t reliably predict which auth method to implement, and security reviews are complicated by the inconsistency.

Decision: Authentication mechanism must be determined by use case, with no mixing:

  • External APIs (public, partner integrations): OAuth2 via Django OAuth Toolkit (DOT) only.
  • Internal service-to-service APIs: JWT only.
  • Browser-based UI APIs: Session only.

Key points:

  • The current view_auth_classes() helper enables all three mechanisms (JwtAuthentication, BearerAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser) globally. Under this ADR, views must explicitly declare only the appropriate class(es) for their use case.
  • Existing APIs need to be audited and potentially refactored; some may need to be split if they currently serve multiple client types.
  • The ADR is clear that the three authentication types are mutually exclusive per endpoint, not layered.

Possible discussion points:

  • This is perhaps the most operationally disruptive ADR in the batch. The current view_auth_classes() pattern is very widely used — is there a migration path that doesn’t require touching every view at once?
  • Many current endpoints are used by both MFEs (session auth) and external integrations (OAuth2). Does “no mixing” mean these use cases need separate endpoint URLs, or is there a sanctioned way to route by client type?
  • JWT for internal service-to-service is well understood, but in practice some internal calls pass through the same public-facing endpoints. How should that be handled?
  • IDA-to-IDA calls currently use JWT-based auth. Does this ADR affect how those are registered or how edx-rest-api-client operates?

ADR 0036 — Normalize Deeply Nested JSON APIs (PR #38305) :magnifying_glass_tilted_left: Open for Review

Problem: Some endpoints (course block trees, OLX structure, progress views) return large, deeply nested JSON payloads that are expensive to parse for both human clients and automated agents, and that bloat response sizes unnecessarily.

Decision: Complex resources must provide a “minimal” representation via a ?view=minimal or ?fields=... query parameter. Nested structures should be flattened where possible, preferring IDs with follow-up endpoints over embedding entire sub-trees inline.

Key points:

  • The Course Blocks API already supports requested_fields and block_types_filter — this ADR formalizes and generalizes that existing pattern platform-wide.
  • The OLX REST API and contentstore course index are also called out as targets.
  • Both response shapes (minimal and full) must be documented in OpenAPI.
  • The ADR recommends measuring payload size reduction and client performance improvements as implementation proceeds.

Possible discussion points:

  • ?view=minimal and ?fields=... serve slightly different purposes (preset minimal view vs. arbitrary field selection) — should the ADR pick one convention rather than leaving both as options?
  • This ADR connects closely to ADR 0032 (pagination). Large block trees are also listed there as a problem case. Is the expectation that ?view=minimal reduces tree depth while pagination addresses list length, or will these need to be applied together?
  • For the course blocks API specifically, “prefer IDs + follow-up endpoints” is a significant behavioral change — have MFEs or other consumers been consulted about whether that pattern works for their rendering needs?

ADR 0037 — API Versioning Strategy (PR #38304) :magnifying_glass_tilted_left: Open for Review

Problem: The platform has a mix of versioned (/api/enrollment/v1/, /api/user/v2/) and non-versioned (/api/course_experience/) endpoints with no consistent rule, and multiple active versions (v0, v1, v2, v3) without a clear “stable” default — making it unclear which version clients should target and when old versions will be removed.

Decision: Treat non-versioned endpoints as the default “current stable” surface where feasible. When a breaking change is required, create a new versioned endpoint (e.g. /api/foo/v2/), then update the non-versioned path to point to the new implementation so the default remains stable. Explicit deprecation policies (schema markers + migration guides + removal timelines) must accompany any version retirement.

Key points:

  • This is a meaningful philosophy shift: the convention moves from “use versioning as the default” toward “non-versioned = current stable, versioning = transition mechanism.”
  • OpenAPI schema must clearly flag deprecated versions and identify the default.
  • The ADR explicitly calls out tooling: SDK generation should automate “latest” selection based on schema metadata.

Possible discussion points:

  • “Non-versioned as default” is somewhat counterintuitive given the current platform conventions — will this confuse teams migrating existing v1 endpoints? Is there clarity on whether existing versioned endpoints need to be renamed/redirected?
  • The strategy of redirecting the non-versioned URL to the new versioned implementation when a breaking change lands is clever, but could confuse clients who pin the non-versioned URL expecting stability. How will this be communicated?
  • There’s potential tension with ADR 0025 (serializers) and ADR 0028 (ViewSets), both of which require versioning if backward compatibility can’t be maintained. Does this ADR supersede or complement those versioning references?
  • How does this interact with the edx-api-doc-tools or any existing API catalog/registry?

ADR 0039 — Modulestore CRUD APIs with Custom Serializers (PR #38302) :magnifying_glass_tilted_left: Open for Review

Problem: Open edX currently lacks comprehensive REST APIs for CRUD operations on modulestore entities (courses and blocks). Modulestore is backed by MongoDB/split rather than Django ORM models, so standard DRF ModelSerializer patterns don’t apply, and existing views use ad-hoc patterns without a consistent REST API surface.

Decision: Implement modulestore CRUD via DRF ViewSets using explicit, non-model serializers paired with a service layer that wraps modulestore() calls. Permissions must enforce authoring-role rules (HasStudioWriteAccess). All operations must be covered by OpenAPI schemas.

Key points:

  • ViewSets use custom get_object() and get_queryset() implementations that delegate to modulestore() rather than ORM querysets.
  • A service layer is explicitly recommended to abstract modulestore interactions — the ADR includes a concrete services.py pattern where permission checks live in the service rather than the view.
  • The ADR recommends starting with read-only (GET) endpoints for course structure and blocks, then adding write operations incrementally.
  • Stable contracts independent of the modulestore backend (MongoDB vs. split) are a stated requirement — important given ongoing Learning Core migration work.

Possible discussion points:

  • The numbering (ADR 0039) jumps ahead of the other ADRs in this batch (0032–0037) — is this intentional, or should it be renumbered for consistency?
  • The service layer pattern described here is broader than just modulestore — should it be elevated into a cross-cutting ADR that applies to all complex resources, not just this one?
  • Learning Core (Blockstore) is the strategic direction for course content. How does this ADR relate to Blockstore-backed APIs? Does “stable contracts independent of modulestore backend” mean this ADR is intended to survive a backend migration, or is it scoped only to the current modulestore?
  • Studio’s contentstore views currently perform CRUD via Python APIs, not REST. What’s the migration/coexistence strategy for Studio MFE calls during the transition?

Cross-Cutting Themes in Batch 2

A few themes that connect across this batch and with the earlier ADRs:

The blocks/course-structure problem appears in three ADRs. ADR 0032 (pagination) calls out the Course Blocks API as an exception; ADR 0036 (nested JSON) targets it directly; ADR 0039 (modulestore CRUD) proposes a new REST surface for the same content. These three probably need to be coordinated — it would be worth ensuring the working group has a unified picture of what the “right” course structure API looks like under all three constraints.

Authentication and permission ADRs are now in tension. ADR 0026 (permission classes, merged) focuses on the DRF authorization surface. ADR 0034 (authentication) now adds strict segregation by client type. Teams migrating views will need to apply both simultaneously — a migration checklist or combined guidance document might help.

Versioning strategy is foundational but arrived late. ADR 0037 defines the versioning philosophy that several earlier ADRs (0025, 0028) already reference. Now that it’s explicit, it’s worth checking whether the earlier ADRs’ versioning language is consistent with ADR 0037’s “non-versioned as default” approach.

ADR numbering gaps. The six ADRs in this batch are numbered 0032, 0033, 0034, 0036, 0037, and 0039 — with gaps at 0035, 0038. It would be useful to know if those numbers are reserved for in-progress drafts or if the numbering needs a tidy-up before merge.


Summary prepared based on the ADR content in PRs #38300, #38301, #38302, #38303, #38304, and #38305. Please refer to the original PRs for full detail and normative text.

Also, I think I would personally prefer to have all of these consolidated into one big, clear “REST API Conventions” OEP rather than many small ADRs, once they’re all accepted. Already it’s a bit confusing as to what info is found in which ADR.

1 Like

@braden your suggestion completely makes sense. Once the ADRs are accepted, we can merge all ADRs into one “REST API Conventions” OEP, Thanks.

2 Likes