CSC/ECE 517 Fall 2025 - E2565. Integration of JoinTeamRequests

From Expertiza_Wiki
Jump to navigation Jump to search

CSC/ECE 517 Fall 2025 - E2565. Integration of JoinTeamRequests

Introduction

This project focuses on the integration and enhancement of the JoinTeamRequests functionality in the Expertiza reimplementation. The JoinTeamRequests feature allows students to request to join existing teams within assignments, providing a structured workflow for team formation and management.

The Expertiza system is a peer-review platform used extensively in academic settings, where students collaborate on assignments in teams. A critical component of this collaboration is the ability for students who are not yet part of a team to request membership in an existing team. This implementation delivers a complete backend API with proper authorization, serialization, and comprehensive test coverage, along with frontend components for managing join team requests.

The goal of this project is to create a seamless, secure, and user-friendly experience for both students requesting to join teams and team members who must review and respond to these requests.

Problem Statement

In the Expertiza system, students need a mechanism to request membership in existing teams when they are not yet part of one. This process involves multiple stakeholders with different roles and permissions:

  • Students without a team need to browse available teams and submit join requests
  • Existing team members need to review incoming requests and decide whether to accept or decline them
  • Administrators need oversight of all join team requests across the system for monitoring and troubleshooting purposes

The existing implementation in the Expertiza reimplementation had several limitations that this project addresses:

Authorization Gaps: The previous implementation lacked fine-grained access control. There was no proper distinction between who could view, create, modify, or act upon join team requests. This created potential security vulnerabilities where unauthorized users could access or manipulate requests.

Insufficient Test Coverage: The existing tests did not cover edge cases or the full range of authorization scenarios, leaving potential bugs undiscovered.

Design Goals

Our implementation focuses on five key design principles that guide all development decisions:

1. Robust Authorization: We implemented fine-grained access control based on user roles (administrator, student) and relationships (request creator, team member). Each API endpoint explicitly defines who can access it, and the authorization logic is centralized in a single method for consistency and maintainability. This ensures that users can only perform actions appropriate to their role and relationship to the request.

2. RESTful API Design: The API follows REST conventions with proper HTTP semantics. We use appropriate HTTP methods (GET for retrieval, POST for creation, PATCH for updates, DELETE for removal) and return meaningful HTTP status codes (200 for success, 201 for creation, 404 for not found, 422 for validation errors). This makes the API intuitive for developers and compatible with standard HTTP clients.

3. Consistent Serialization: All API responses pass through ActiveModel Serializers, ensuring consistent JSON structure across all endpoints. This includes nested representations of related objects (participant and team information) that the frontend needs to display meaningful information to users without making additional API calls.

4. Comprehensive Testing: Every endpoint and authorization scenario is covered by RSpec tests. We test both the "happy path" (expected behavior) and edge cases (error conditions, unauthorized access attempts). This gives us confidence that the implementation works correctly and will continue to work as the codebase evolves.

5. Frontend Integration: The backend is designed with frontend consumption in mind. Response formats include all necessary data for UI rendering, error messages are user-friendly, and filtering endpoints reduce the need for client-side data processing.

Implementation

Backend Architecture Overview

The backend implementation follows the standard Rails MVC pattern with additional layers for serialization and authorization. The architecture consists of:

  • Controller: Handles HTTP requests, enforces authorization, and coordinates between models and serializers
  • Model: Defines data structure, relationships, and validations
  • Serializer: Transforms model data into consistent JSON responses
  • Routes: Maps URLs to controller actions


Controller: Api::V1::JoinTeamRequestsController

The controller is the heart of our implementation, located at app/controllers/api/v1/join_team_requests_controller.rb. It handles all join team request operations and enforces authorization rules.

Constants and Status Management

We define status constants at the class level to ensure consistency throughout the codebase and avoid magic strings. This approach makes the code more maintainable—if we ever need to change a status value, we only need to update it in one place.

class Api::V1::JoinTeamRequestsController < ApplicationController
  # Constants used to indicate status for the request
  # Using constants prevents typos and enables easy refactoring
  PENDING = 'PENDING'
  DECLINED = 'DECLINED'
  ACCEPTED = 'ACCEPTED'

Before Actions

Rails before_action filters allow us to extract common logic that runs before specific controller actions. This follows the DRY (Don't Repeat Yourself) principle and ensures consistent behavior across related endpoints.

  # This filter runs before the create action, checking if the team is full
  # Checking this early prevents unnecessary database operations if the request would fail anyway
  before_action :check_team_status, only: [:create]

  # This filter runs before the specified actions, finding the join team request
  # Centralizing this lookup ensures consistent 404 handling and reduces code duplication
  before_action :find_request, only: %i[show update destroy decline accept]

Authorization Implementation

The action_allowed? method is the cornerstone of our authorization system. Unlike simple role-based checks, our implementation considers both the user's role AND their relationship to the specific resource being accessed. This provides security while maintaining usability.

The following table summarizes the authorization rules:

Action Authorization Rule Rationale
index Only administrators Viewing all requests system-wide is an administrative function
create Any student All students should be able to request to join teams
show Request creator OR team member Both parties need to see request details
update, destroy Only the request creator Only the requester should modify or withdraw their request
accept, decline Only team members Only existing team members can decide on membership
for_team, by_user, pending Any student Filtered views help students find relevant requests

The implementation of this authorization logic demonstrates several important patterns:

def action_allowed?
  case params[:action]
  when 'index'
    # Only administrators can view all join team requests
    # This prevents students from seeing requests for teams they're not part of
    current_user_has_admin_privileges?
  
  when 'create'
    # Any student can create a join team request
    # Additional validation (participant check, team membership) happens in the action itself
    current_user_has_student_privileges?
  
  when 'show'
    # The participant who made the request OR any team member can view it
    # This allows both parties to stay informed about the request status
    return false unless current_user_has_student_privileges?
    
    # Load the request for authorization check (memoized to avoid duplicate queries)
    @join_team_request = JoinTeamRequest.find_by(id: params[:id]) unless @join_team_request
    return false unless @join_team_request
    
    # Check if user is either the creator or a team member
    current_user_is_request_creator? || current_user_is_team_member?
  
  when 'update', 'destroy'
    # Only the participant who created the request can update or delete it
    # This prevents team members from modifying requests they didn't create
    return false unless current_user_has_student_privileges?
    @join_team_request = JoinTeamRequest.find_by(id: params[:id]) unless @join_team_request
    return false unless @join_team_request
    current_user_is_request_creator?
  
  when 'decline', 'accept'
    # Only team members of the target team can accept/decline a request
    # The requester themselves cannot accept their own request
    return false unless current_user_has_student_privileges?
    @join_team_request = JoinTeamRequest.find_by(id: params[:id]) unless @join_team_request
    return false unless @join_team_request
    current_user_is_team_member?
  
  when 'for_team', 'by_user', 'pending'
    # Students can view filtered lists
    # These endpoints are scoped appropriately in their implementations
    current_user_has_student_privileges?
  
  else
    # Default: deny access for any unrecognized actions
    # This is a security best practice - fail closed
    false
  end
end

API Endpoints

Each endpoint is designed to serve a specific use case. Below we detail each endpoint with its purpose, implementation, and usage examples.

1. Pending - Get Pending Requests

This convenience endpoint returns only pending requests, helping users focus on requests that require action.

# GET api/v1/join_team_requests/pending
# Get all pending join team requests
def pending
  join_team_requests = JoinTeamRequest.where(reply_status: PENDING).includes(:participant, :team)
  render json: join_team_requests, each_serializer: JoinTeamRequestSerializer, status: :ok
end
2. Create - Submit New Request

The create endpoint is the most complex, with multiple validation checks to ensure data integrity. It demonstrates defensive programming practices by validating inputs before creating records.

# POST api/v1/join_team_requests
# Creates a new join team request with comprehensive validation
def create
  # Step 1: Verify the user is a participant in the assignment
  # This prevents users from creating requests for assignments they're not enrolled in
  participant = AssignmentParticipant.find_by(user_id: @current_user.id, parent_id: params[:assignment_id])
  
  unless participant
    return render json: { error: 'You are not a participant in this assignment' }, status: :unprocessable_entity
  end

  # Step 2: Verify the target team exists
  team = Team.find_by(id: params[:team_id])
  unless team
    return render json: { error: 'Team not found' }, status: :not_found
  end

  # Step 3: Check if user already belongs to the team
  # This prevents unnecessary requests and potential confusion
  if team.participants.include?(participant)
    return render json: { error: 'You already belong to this team' }, status: :unprocessable_entity
  end

  # Step 4: Check for duplicate pending requests
  # Allowing multiple pending requests to the same team would be confusing
  existing_request = JoinTeamRequest.find_by(
    participant_id: participant.id,
    team_id: team.id,
    reply_status: PENDING
  )
  
  if existing_request
    return render json: { error: 'You already have a pending request for this team' }, status: :unprocessable_entity
  end

  # Step 5: Create the request with all validated data
  join_team_request = JoinTeamRequest.new(
    participant_id: participant.id,
    team_id: team.id,
    comments: params[:comments],
    reply_status: PENDING  # All new requests start as pending
  )

  if join_team_request.save
    render json: join_team_request, serializer: JoinTeamRequestSerializer, status: :created
  else
    render json: { errors: join_team_request.errors.full_messages }, status: :unprocessable_entity
  end
rescue ActiveRecord::RecordNotFound => e
  render json: { error: e.message }, status: :not_found
end
Join Request Form

Figure 1: The join request form allows students to select a team and optionally add comments explaining why they want to join. The form validates that the student is not already on a team and that the target team is not full.

3. Update - Modify Request Comments

The update endpoint allows requesters to modify their request comments. We intentionally restrict updates to comments only—status changes must go through the accept/decline endpoints.

# PATCH/PUT api/v1/join_team_requests/1
# Updates a join team request (comments only, not status)
def update
  # Explicitly only allow updating comments
  # This prevents requesters from accepting their own requests
  if @join_team_request.update(comments: params[:comments])
    render json: @join_team_request, serializer: JoinTeamRequestSerializer, status: :ok
  else
    render json: { errors: @join_team_request.errors.full_messages }, status: :unprocessable_entity
  end
end
4. Destroy - Delete Request

Students can withdraw their join requests at any time. This is useful if they've found another team or changed their mind.

# DELETE api/v1/join_team_requests/1
# Deletes a join team request (withdraw the request)
def destroy
  if @join_team_request.destroy
    render json: { message: 'Join team request was successfully deleted' }, status: :ok
  else
    render json: { error: 'Failed to delete join team request' }, status: :unprocessable_entity
  end
end
5. Accept - Approve and Add Member

The accept endpoint is critical—it not only updates the request status but also adds the requester to the team. This transactional operation ensures data consistency.

# PATCH api/v1/join_team_requests/1/accept
# Accept a join team request and add the participant to the team
def accept
  # Validation 1: Ensure request hasn't already been processed
  # This prevents race conditions and duplicate team membership
  unless @join_team_request.reply_status == PENDING
    return render json: { error: 'This request has already been processed' }, status: :unprocessable_entity
  end

  # Validation 2: Check team capacity before accepting
  team = @join_team_request.team
  if team.full?
    return render json: { error: 'Team is full' }, status: :unprocessable_entity
  end

  # Attempt to add the participant to the team
  begin
    result = team.add_member(@join_team_request.participant)
    
    if result[:success]
      # Only update the request status after successfully adding the member
      @join_team_request.reply_status = ACCEPTED
      @join_team_request.save
      render json: { 
        message: 'Join team request accepted successfully', 
        join_team_request: JoinTeamRequestSerializer.new(@join_team_request).as_json
      }, status: :ok
    else
      render json: { error: result[:error] }, status: :unprocessable_entity
    end
  rescue StandardError => e
    # Catch any unexpected errors during the add_member operation
    render json: { error: e.message }, status: :unprocessable_entity
  end
end


Accept Request
Accept Request
Accept Request Confirmation
Accept Request Confirmation


Figure 2: When a team member clicks the Accept button, a confirmation dialog appears showing the requester's details. Upon confirmation, the requester is added to the team and notified of the acceptance.

6. Decline - Reject Request

The decline endpoint allows team members to reject join requests. Unlike delete, decline preserves the request record for audit purposes.

# PATCH api/v1/join_team_requests/1/decline
# Decline a join team request
def decline
  # Prevent re-processing of already handled requests
  unless @join_team_request.reply_status == PENDING
    return render json: { error: 'This request has already been processed' }, status: :unprocessable_entity
  end

  @join_team_request.reply_status = DECLINED
  if @join_team_request.save
    render json: { 
      message: 'Join team request declined successfully',
      join_team_request: JoinTeamRequestSerializer.new(@join_team_request).as_json
    }, status: :ok
  else
    render json: { errors: @join_team_request.errors.full_messages }, status: :unprocessable_entity
  end
end

Private Helper Methods

The private methods encapsulate reusable logic and keep the public action methods clean and focused.

private

# Checks if the team is full before allowing request creation
# Running this as a before_action prevents unnecessary database operations
def check_team_status
  team = Team.find(params[:team_id])
  if team.full?
    render json: { message: 'This team is full.' }, status: :unprocessable_entity
  end
end

# Finds the join team request by ID
# Raises ActiveRecord::RecordNotFound if not found, which Rails converts to a 404 response
def find_request
  @join_team_request = JoinTeamRequest.find(params[:id])
end

# Permits specified parameters for join team requests
# This is a security measure to prevent mass assignment vulnerabilities
def join_team_request_params
  params.require(:join_team_request).permit(:comments, :reply_status)
end

# Helper method to check if current user is the creator of the request
# Uses safe navigation (&.) to handle nil cases gracefully
def current_user_is_request_creator?
  return false unless @join_team_request && @current_user
  
  participant = Participant.find_by(id: @join_team_request.participant_id)
  participant&.user_id == @current_user.id
end

# Helper method to check if current user is a member of the target team
# This involves looking up the user's participant record for the relevant assignment
def current_user_is_team_member?
  return false unless @join_team_request && @current_user
  
  team = Team.find_by(id: @join_team_request.team_id)
  return false unless team
  
  # Find the participant record for the current user in the same assignment
  if team.is_a?(AssignmentTeam)
    participant = AssignmentParticipant.find_by(
      user_id: @current_user.id,
      parent_id: team.parent_id
    )
    return false unless participant
    
    # Check if this participant is in the team's participants list
    team.participants.include?(participant)
  else
    false
  end
end

Model: JoinTeamRequest

The model is intentionally simple, following the Rails principle of keeping models focused on data concerns. It defines relationships and validations, delegating business logic to the controller.

Located at app/models/join_team_request.rb:

class JoinTeamRequest < ApplicationRecord
  # Associations define the relationships with other models
  belongs_to :team
  belongs_to :participant
  
  # Status validation ensures only valid statuses can be saved
  # This is a database-level safeguard complementing the controller logic
  ACCEPTED_STATUSES = %w[ACCEPTED DECLINED PENDING]
  validates :reply_status, inclusion: { in: ACCEPTED_STATUSES }
end

Serializer: JoinTeamRequestSerializer

The serializer transforms model data into a consistent JSON format suitable for frontend consumption. By including related data (participant and team details), we reduce the number of API calls the frontend needs to make.

class JoinTeamRequestSerializer < ActiveModel::Serializer
  # Top-level attributes from the JoinTeamRequest model
  attributes :id, :reply_status, :comments, :created_at, :updated_at
  
  # Declare associations (these methods below override default behavior)
  belongs_to :participant
  belongs_to :team
  
  # Custom participant representation with user details
  # This saves the frontend from making separate API calls for user information
  def participant
    {
      id: object.participant.id,
      user_id: object.participant.user_id,
      user_name: object.participant.user&.name,
      user_full_name: object.participant.user&.full_name
    }
  end
  
  # Custom team representation with essential details
  def team
    {
      id: object.team.id,
      name: object.team.name,
      parent_id: object.team.parent_id
    }
  end
end

Routes Configuration

Our routes follow RESTful conventions with additional collection and member routes for specialized operations. The route structure makes the API intuitive and self-documenting.

Added to config/routes.rb:

resources :join_team_requests do
  collection do
    # Filtering endpoints - operate on the collection, not a specific resource
    get 'for_team/:team_id', action: :for_team
    get 'by_user/:user_id', action: :by_user
    get 'pending', action: :pending
  end
  member do
    # Action endpoints - operate on a specific resource
    patch 'accept', action: :accept
    patch 'decline', action: :decline
  end
end

This configuration generates the following routes:

HTTP Method URL Pattern Controller Action Purpose
GET /api/v1/join_team_requests index List all requests (admin)
GET /api/v1/join_team_requests/:id show View single request
POST /api/v1/join_team_requests create Submit new request
PATCH/PUT /api/v1/join_team_requests/:id update Modify request
DELETE /api/v1/join_team_requests/:id destroy Delete request
GET /api/v1/join_team_requests/for_team/:team_id for_team Filter by team
GET /api/v1/join_team_requests/by_user/:user_id by_user Filter by user
GET /api/v1/join_team_requests/pending pending Get pending only
PATCH /api/v1/join_team_requests/:id/accept accept Accept request
PATCH /api/v1/join_team_requests/:id/decline decline Decline request


API Response Format

Consistent response formats make frontend development more predictable. All successful responses follow the same structure, and error responses are clearly formatted.

Success Response Example (Single Request)

{
  "id": 1,
  "reply_status": "PENDING",
  "comments": "I would like to join your team for the project.",
  "created_at": "2025-01-15T10:30:00.000Z",
  "updated_at": "2025-01-15T10:30:00.000Z",
  "participant": {
    "id": 5,
    "user_id": 12,
    "user_name": "student1",
    "user_full_name": "John Doe"
  },
  "team": {
    "id": 3,
    "name": "Team Alpha",
    "parent_id": 1
  }
}

Error Response Examples

Validation errors return descriptive messages that can be displayed directly to users:

{
  "error": "You already belong to this team"
}
{
  "error": "This request has already been processed"
}

Model validation errors return an array format:

{
  "errors": ["Reply status is not included in the list"]
}

Testing

Test Philosophy

Our testing approach follows the principle of testing behavior rather than implementation. Each test describes a user scenario and verifies that the system responds correctly. This makes tests resilient to refactoring and serves as executable documentation.

Test Coverage

Comprehensive RSpec tests are implemented in spec/requests/api/v1/join_team_requests_spec.rb. The tests cover all endpoints and authorization scenarios.

Test Categories

1. Index Tests

These tests verify that only administrators can access the full list of join team requests:

  • Returns all join team requests for admin users with proper serialization
  • Returns 403 Forbidden for non-admin users attempting to access the list

2. Show Tests

Show tests verify the authorization rules for viewing individual requests:

  • Returns request details when accessed by the request creator
  • Returns request details when accessed by a team member
  • Returns 403 Forbidden when accessed by an unrelated user

3. Create Tests

Create tests cover both success cases and all validation scenarios:

  • Creates request successfully with valid parameters and returns 201 Created
  • Returns 422 when user tries to create a duplicate pending request
  • Returns 422 when user already belongs to the target team
  • Returns 422 when the target team is full
  • Returns 422 when user is not a participant in the assignment

4. Update Tests

Update tests verify that only creators can modify their requests:

  • Updates comments successfully for the request creator
  • Returns 403 when a non-creator attempts to update

5. Destroy Tests

Destroy tests verify the deletion authorization:

  • Deletes request successfully for the creator
  • Returns 403 when a non-creator attempts to delete

6. Accept Tests

Accept tests cover the complex acceptance workflow:

  • Accepts pending request, adds member to team, and updates status to ACCEPTED
  • Returns 422 when attempting to accept an already-processed request
  • Returns 422 when the target team is full
  • Returns 403 when a non-team-member attempts to accept

7. Decline Tests

Decline tests mirror the accept tests for the decline workflow:

  • Declines pending request and updates status to DECLINED
  • Returns 422 when attempting to decline an already-processed request
  • Returns 403 when a non-team-member attempts to decline

Running Tests

The following commands can be used to run the test suite:

# Run all join team request tests
bundle exec rspec spec/requests/api/v1/join_team_requests_spec.rb

# Run with documentation format for readable output
bundle exec rspec spec/requests/api/v1/join_team_requests_spec.rb --format documentation

# Run a specific test by line number
bundle exec rspec spec/requests/api/v1/join_team_requests_spec.rb:50

# Run tests with coverage report
COVERAGE=true bundle exec rspec spec/requests/api/v1/join_team_requests_spec.rb

Frontend Integration

Component Architecture

The frontend implementation uses React with TypeScript for type safety. Components are organized following a feature-based structure where all join team request related components are grouped together.

React Components

The frontend implementation includes the following React components:

JoinTeamRequestList: Displays all join team requests with filtering capabilities. Users can filter by status (pending, accepted, declined) and search by team name or requester name. Each row in the list is clickable to expand and show the full request details including comments.

JoinTeamRequestForm: A form component for submitting new join requests. It includes team selection via a searchable dropdown and an optional comments field. The form performs client-side validation before submission.

JoinTeamRequestActions: A component containing Accept and Decline buttons for team members. These buttons are only rendered for users who are members of the target team. Clicking either button shows a confirmation modal before processing.

PendingRequestsBadge: A notification badge component that shows the count of pending requests. This appears in the navigation bar to alert team members of incoming requests that need attention.

API Integration

The frontend communicates with the backend using axios. All API calls are centralized in a service file for maintainability:

// src/services/joinTeamRequestService.ts
import axiosClient from '../utils/axios_client';

interface JoinTeamRequestData {
  team_id: number;
  assignment_id: number;
  comments?: string;
}

// Fetch all join team requests (admin only)
export const getJoinTeamRequests = () => 
  axiosClient.get('/join_team_requests');

// Fetch requests for a specific team
export const getTeamRequests = (teamId: number) => 
  axiosClient.get(`/join_team_requests/for_team/${teamId}`);

// Fetch requests by a specific user
export const getUserRequests = (userId: number) => 
  axiosClient.get(`/join_team_requests/by_user/${userId}`);

// Fetch only pending requests
export const getPendingRequests = () => 
  axiosClient.get('/join_team_requests/pending');

// Create a new join request
export const createJoinRequest = (data: JoinTeamRequestData) => 
  axiosClient.post('/join_team_requests', data);

// Update request comments
export const updateJoinRequest = (id: number, comments: string) => 
  axiosClient.patch(`/join_team_requests/${id}`, { comments });

// Delete a join request
export const deleteJoinRequest = (id: number) => 
  axiosClient.delete(`/join_team_requests/${id}`);

// Accept a join request
export const acceptRequest = (id: number) => 
  axiosClient.patch(`/join_team_requests/${id}/accept`);

// Decline a join request
export const declineRequest = (id: number) => 
  axiosClient.patch(`/join_team_requests/${id}/decline`);


Refactoring Improvements

This section highlights the key improvements made through refactoring, comparing the original implementation with our enhanced version.

Before: Scattered Authorization Logic

The original implementation had authorization checks scattered throughout each action method, leading to code duplication and inconsistency:

# BEFORE: Authorization mixed with business logic
def accept
  if current_user.role != 'student'
    render json: { error: 'Unauthorized' }, status: :forbidden
    return
  end
  # ... more logic
end

def decline
  if current_user.role != 'student'
    render json: { error: 'Unauthorized' }, status: :forbidden
    return
  end
  # ... duplicate checks
end

After: Centralized Authorization

Our refactored implementation centralizes all authorization in the action_allowed? method:

# AFTER: Centralized authorization with clear rules
def action_allowed?
  case params[:action]
  when 'decline', 'accept'
    return false unless current_user_has_student_privileges?
    @join_team_request = JoinTeamRequest.find_by(id: params[:id]) unless @join_team_request
    return false unless @join_team_request
    current_user_is_team_member?
  # ... other cases
  end
end

Benefits:

  • Single source of truth for authorization rules
  • Easier to audit and update security policies
  • Reduced code duplication
  • Consistent behavior across all endpoints

Before: Inconsistent Response Formats

The original implementation returned data in various formats, making frontend integration difficult:

# BEFORE: Inconsistent response structure
def show
  render json: @join_team_request.attributes
end

def index
  render json: JoinTeamRequest.all.map { |r| { id: r.id, status: r.reply_status } }
end

After: Serializer-Based Responses

Our implementation uses ActiveModel Serializers for consistent output:

# AFTER: Consistent serialized responses
def show
  render json: @join_team_request, serializer: JoinTeamRequestSerializer, status: :ok
end

def index
  join_team_requests = JoinTeamRequest.includes(:participant, :team).all
  render json: join_team_requests, each_serializer: JoinTeamRequestSerializer, status: :ok
end

Benefits:

  • Consistent JSON structure across all endpoints
  • Easier frontend development with predictable data shapes
  • Centralized control over what data is exposed
  • Automatic handling of nested relationships

Conclusion

This project successfully delivers a complete, production-ready implementation of the JoinTeamRequests feature for the Expertiza reimplementation. The key achievements include:

  • Security: Fine-grained authorization ensures users can only access and modify resources appropriate to their role
  • Usability: Filtering endpoints and consistent response formats make the API easy to consume
  • Maintainability: Centralized logic, comprehensive tests, and clear documentation facilitate future development
  • Reliability: Thorough testing covers both happy paths and edge cases

References

  1. Expertiza Wiki - Project documentation and guidelines
  2. Rails API Documentation - Ruby on Rails framework reference
  3. RSpec Documentation - Testing framework documentation
  4. ActiveModel Serializers - JSON serialization library
  5. Expertiza Reimplementation Backend - Source repository
  6. Expertiza Reimplementation Frontend - Frontend repository