_private/qwestly-docs/Engineering/API-endpoint-structure.md

API Endpoint Structure Guide

Overview

This document outlines the API endpoint structure for the Qwestly platform, following RESTful principles while implementing smart path inference for user-contextual operations.

Core Principles

1. RESTful Design

  • Use standard HTTP methods (GET, POST, PUT, DELETE, PATCH)
  • Follow RESTful resource naming conventions
  • Maintain consistent response formats
  • Use appropriate HTTP status codes

2. Smart Path Inference

  • User Context: When dealing with resources that belong to the authenticated user, omit the user/candidate ID from the path
  • Latest Resource Context: When dealing with the most recent instance of a resource (e.g., latest interview), omit the specific ID from the path
  • Explicit IDs: Use explicit IDs in paths when dealing with specific, non-user-owned resources or when multiple resources exist

Endpoint Structure Patterns

User-Owned Resources (No ID in Path)

Professional Summary

GET    /api/summary                    # Get current user's professional summary
POST   /api/summary                    # Create/update current user's summary
PUT    /api/summary                    # Replace current user's summary
PATCH  /api/summary                    # Partially update current user's summary
DELETE /api/summary                    # Delete current user's summary

Background Information

GET    /api/background                 # Get current user's background
POST   /api/background                 # Create/update current user's background
PUT    /api/background                 # Replace current user's background
PATCH  /api/background                 # Partially update current user's background
DELETE /api/background                 # Delete current user's background

Profile Data

GET    /api/profile                    # Get current user's profile
POST   /api/profile                    # Create/update current user's profile
PUT    /api/profile                    # Replace current user's profile
PATCH  /api/profile                    # Partially update current user's profile
DELETE /api/profile                    # Delete current user's profile

Preferences

GET    /api/preferences                # Get current user's preferences
POST   /api/preferences                # Create/update current user's preferences
PUT    /api/preferences                # Replace current user's preferences
PATCH  /api/preferences                # Partially update current user's preferences
DELETE /api/preferences                # Delete current user's preferences

Latest Resource Context (No ID in Path)

Resumption Context (Latest Interview)

GET    /api/resumption-context         # Get resumption context from latest interview
POST   /api/resumption-context         # Create resumption context for latest interview
PUT    /api/resumption-context         # Replace resumption context for latest interview
PATCH  /api/resumption-context         # Partially update resumption context for latest interview
DELETE /api/resumption-context         # Delete resumption context for latest interview

Current Interview

GET    /api/interview                  # Get current/latest interview
POST   /api/interview                  # Create new interview
PUT    /api/interview                  # Replace current interview
PATCH  /api/interview                  # Partially update current interview
DELETE /api/interview                  # Delete current interview

Explicit Resource Management (With IDs in Path)

Specific Interviews

GET    /api/interviews/{interview_id}                    # Get specific interview
PUT    /api/interviews/{interview_id}                    # Replace specific interview
PATCH  /api/interviews/{interview_id}                    # Partially update specific interview
DELETE /api/interviews/{interview_id}                    # Delete specific interview

GET    /api/interviews/{interview_id}/resumption-context # Get resumption context for specific interview
POST   /api/interviews/{interview_id}/resumption-context # Create resumption context for specific interview
PUT    /api/interviews/{interview_id}/resumption-context # Replace resumption context for specific interview
PATCH  /api/interviews/{interview_id}/resumption-context # Partially update resumption context for specific interview
DELETE /api/interviews/{interview_id}/resumption-context # Delete resumption context for specific interview

Authentication & Authorization

Endpoint Protection

  • User endpoints (/api/summary, /api/background): Require user authentication
  • Admin endpoints (/api/admin/*): Require admin authentication
  • Public endpoints (/api/waitlist): No authentication required

User Context Resolution

  • User context is determined from the JWT token or session
  • No need to pass user ID in request body for user-owned resources
  • Admin endpoints explicitly require candidate ID for cross-user operations

Response Format Standards

Success Response

{
  "success": true,
  "data": {
    // Resource data here
  },
  "message": "Operation completed successfully"
}

Error Response

{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable error message",
    "details": "Additional error context if available"
  }
}

Pagination Response (for list endpoints)

{
  "success": true,
  "data": {
    "items": [...],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 100,
      "totalPages": 5
    }
  }
}

HTTP Status Codes

Success Codes

  • 200 OK: Request successful
  • 201 Created: Resource created successfully
  • 204 No Content: Request successful, no content to return

Client Error Codes

  • 400 Bad Request: Invalid request data
  • 401 Unauthorized: Authentication required
  • 403 Forbidden: Insufficient permissions
  • 404 Not Found: Resource not found
  • 409 Conflict: Resource conflict (e.g., duplicate entry)
  • 422 Unprocessable Entity: Validation errors

Server Error Codes

  • 500 Internal Server Error: Unexpected server error
  • 503 Service Unavailable: Service temporarily unavailable

Implementation Guidelines

1. Route Handler Structure

// Example: /api/enhanced/candidates/[[...slug]]/route.ts
import ApiRouter from "@/lib/api-router";
import { NextResponse } from 'next/server';
import { isCurrentUserOrAdmin } from "@/services/auth/getPageAuth0Client";

const router = new ApiRouter();

// GET /api/enhanced/candidates/{userId} - get a candidate profile
router.get("/{userId}", async (req, vars) => {
  if (!await isCurrentUserOrAdmin(vars.userId)) {
    return NextResponse.json(
      { error: 'You are not authorized to access this resource' },
      { status: 403 }
    );
  }

  const userId = vars.userId;
  // ... fetch and return candidate data
});

// PATCH /api/enhanced/candidates/{userId} - update a candidate profile
router.patch("/{userId}", async (req, vars) => {
  const userId = vars.userId;

  if (!await isCurrentUserOrAdmin(vars.userId)) {
    return NextResponse.json(
      { error: 'You are not authorized to access this resource' },
      { status: 403 }
    );
  }
  
  const body = await req.json();
  // ... update candidate data
});

export const { GET, POST, PATCH, DELETE } = router.exportRoutes();

2. User Context Resolution

// Helper functions from getPageAuth0Client.ts
import { getUserId, getUser, isCurrentUserOrAdmin } from "@/services/auth/getPageAuth0Client";

// Get current user's ID from JWT session
const currentUserId = await getUserId(); // Returns user.sub from Auth0

// Get full user object
const user = await getUser(); // Returns Auth0 user object

// Check if user can access specific resource
const canAccess = await isCurrentUserOrAdmin(targetUserId);
// Returns true if current user is admin OR if current user matches target user

3. Error Handling

// Centralized error handler
function handleApiError(error: unknown) {
  if (error instanceof ValidationError) {
    return Response.json({
      success: false,
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Invalid request data',
        details: error.details
      }
    }, { status: 422 });
  }
  
  // Handle other error types...
  
  return Response.json({
    success: false,
    error: {
      code: 'INTERNAL_ERROR',
      message: 'An unexpected error occurred'
    }
  }, { status: 500 });
}

4. Authentication & Authorization Patterns

// Admin-only endpoints
router.get("/", async (req) => {
  const isUserAdmin = await isAdmin();
  if (!isUserAdmin) {
    return NextResponse.json(
      { error: 'Admin access required' },
      { status: 403 }
    );
  }
  // ... admin logic
});

// User-specific endpoints with authorization
router.get("/{userId}", async (req, vars) => {
  if (!await isCurrentUserOrAdmin(vars.userId)) {
    return NextResponse.json(
      { error: 'You are not authorized to access this resource' },
      { status: 403 }
    );
  }
  // ... user-specific logic
});

// Public endpoints (no authentication required)
router.post("/", async (req) => {
  // No auth check needed
  // ... public logic
});

5. NextJS Catchall Route Structure

// File: src/app/api/enhanced/candidates/[[...slug]]/route.ts
// This creates a catchall route that handles:
// - /api/enhanced/candidates/ (no slug)
// - /api/enhanced/candidates/{userId} (single slug)
// - /api/enhanced/candidates/{userId}/subresource (multiple slugs)

const router = new ApiRouter();

// Handle root path
router.get("/", async (req) => {
  // Handle /api/enhanced/candidates/
});

// Handle single parameter
router.get("/{userId}", async (req, vars) => {
  // Handle /api/enhanced/candidates/{userId}
  const userId = vars.userId;
});

// Handle nested resources
router.get("/{userId}/subresource", async (req, vars) => {
  // Handle /api/enhanced/candidates/{userId}/subresource
  const userId = vars.userId;
});

6. Error Handling

// Standard error response format
return NextResponse.json(
  { error: 'Error message here' },
  { status: 400 }
);

// Success response format
return NextResponse.json(data, { status: 200 });

// Created response format
return NextResponse.json(data, { status: 201 });

// Error with custom status code
const statusCode = (error as any).statusCode;
return NextResponse.json(
  { error: (error as Error).message },
  { status: statusCode }
);

Testing Guidelines

Unit Tests

  • Test each endpoint with valid and invalid data
  • Verify authentication and authorization logic
  • Test error handling and status codes

Integration Tests

  • Test complete request/response cycles
  • Verify database operations
  • Test authentication flow

End-to-End Tests

  • Test complete user workflows
  • Verify frontend-backend integration
  • Test error scenarios in real usage

Frontend Usage

The project uses a custom APIRequest library that handles authentication, error handling, and response formatting automatically. This is preferred over the direct fetch approach.

import APIRequest from "@/lib/api-request";

// GET request
const response = await APIRequest.get<ICandidate>(`enhanced/candidates/${userId}`);
if (response.error) {
  console.error('API Error:', response.error);
  return null;
}
return response.data;

// POST request
const createResponse = await APIRequest.post<ICandidate, Partial<ICandidate>>(
  "enhanced/candidates",
  { user_id: userId, name: "John Doe" }
);

// PATCH request
const updateResponse = await APIRequest.patch<ICandidate, Partial<ICandidate>>(
  `enhanced/candidates/${userId}`,
  { name: "Updated Name" }
);

Best Practices

1. Consistency

  • Use consistent naming across all endpoints
  • Maintain consistent response formats
  • Follow established error handling patterns

2. Security

  • Always validate user permissions
  • Sanitize input data
  • Use HTTPS in production
  • Implement rate limiting

3. Performance

  • Implement caching where appropriate
  • Use database indexes for queries
  • Consider pagination for large datasets
  • Monitor API response times

4. Documentation

  • Keep this document updated
  • Provide examples for each endpoint
  • Document any deviations from standard patterns
  • Include troubleshooting guides

Conclusion

This API structure provides a clean, intuitive interface that balances RESTful principles with practical usability. The smart path inference reduces complexity for common operations while maintaining explicit control when needed. Regular reviews and updates ensure the API continues to meet evolving requirements.