Skip to content

πŸ“‘ Backend Hono API Contract Reference ​

All backend API routers reside in the [apps/api/src/routes/](file:///Users/ulad/Documents/Pets/BikeTrackV2/bike-maps/apps/api/src/routes/) folder and are powered by Hono combined with Zod Validator middleware. All endpoints (except Strava webhook callbacks) require a Firebase Bearer token: Authorization: Bearer <firebase_uid_token>.


πŸ—ΊοΈ 1. Routing Service (/routing) ​

A. Point-to-Point Path Calculation ​

Computes route geometry, elevation, and surface types.

  • Method & Path: POST /routing/calculate
  • Request Headers: Authorization: Bearer <token>
  • Request Body:
typescript
{
  waypoints: Array<{ lat: number; lng: number }>; // Min 2 coordinates
  profile?: "road" | "gravel" | "mtb" | "commute" | "direct"; // Default: "road"
  alternativeIdx?: number;
  avoidanceZones?: Array<{
    id: string;
    coordinates: number[][]; // Bounding coordinates
    isInvalid: boolean;
  }>;
}
  • Response (200 OK):
typescript
{
  distanceMeters: number;
  routeGeometry: {
    type: "LineString";
    coordinates: Array<[number, number]>; // strict [longitude, latitude]
  };
  elevationProfile: Array<{
    distance: number; // meters from start
    elevation: number; // meters above sea level
    lat: number;
    lng: number;
    surfaceType: string; // "asphalt" | "gravel" | "dirt" | "unknown"
  }>;
  stats: {
    elevationGain: number;
    elevationLoss: number;
    surfaceBreakdown: {
      asphalt: number; // percentage
      gravel: number;
      dirt: number;
      unknown: number;
    }
  };
}

B. Round Trip Loop Generation ​

Creates random loop routes from a start point matching target distance metrics.

  • Method & Path: POST /routing/loop
  • Request Body:
typescript
{
  start: { lat: number; lng: number };
  targetDistanceKm: number;
  elevationPreference: "flat" | "hilly" | "indifferent";
  directionBias: "clockwise" | "counterclockwise" | "random";
  profile: "road" | "gravel" | "mtb";
}
  • Response (200 OK): Identical structure to point-to-point /calculate result.

βœ‚οΈ 2. Route Library & Crop Engine (/api/routes) ​

A. Crop Route (Anti-Doxing & Trim) ​

Removes exact start/end segments to hide home location.

  • Method & Path: PATCH /api/routes/:id/crop
  • Request Body:
typescript
{
  startTrimMeters?: number; // Distance to slice from beginning
  endTrimMeters?: number;   // Distance to slice from ending
}
  • Response (200 OK):
typescript
{
  success: true,
  route: {
    id: string;
    userId: string;
    sourceActivityId: string | null;
    name: string;
    distance: number; // in kilometers
    polyline: string; // encoded cropped polyline string
    isEbike: boolean;
    createdAt: string;
  }
}
  • Response (400 Bad Request):
typescript
{
  error: "Invalid crop distances. Crop must leave at least 100 meters of route."
}

πŸ“ 3. Point of Interest (POI) Service (/api/pois) ​

A. Create Point of Interest ​

Places a custom marker onto the global coordinate pool or attaches it to a route.

  • Method & Path: POST /api/pois
  • Request Body:
typescript
{
  name: string;
  type: "WATER" | "FOOD" | "HAZARD" | "VIEWPOINT" | "CAMPING" | "GENERAL";
  coordinates: { lng: number; lat: number } | [number, number]; // GeoJSON standard
  description?: string;
  scope?: "global" | "track"; // Default: "global"
  visibility?: "public" | "private"; // Default: "public"
  routeId?: string; // Optional UUID link
}
  • Response (201 Created):
typescript
{
  id: string;
  name: string;
  type: "water" | "food" | "hazard" | "viewpoint" | "camping" | "general";
  source: "user";
  coordinates: { lng: number; lat: number };
  scope: "global" | "track";
  visibility: "public" | "private";
  description?: string;
  routeId?: string;
}

B. List Global POIs (With optional Bounding Box filter) ​

Fetches user POIs, with optional screen spatial limits.

  • Method & Path: GET /api/pois
  • Query Params: minLng, minLat, maxLng, maxLat (All must be present to activate spatial index search).
  • Response (200 OK):
typescript
{
  items: Array<{
    id: string;
    name: string;
    type: string;
    coordinates: { lng: number; lat: number };
    scope: string;
    visibility: string;
    description?: string;
  }>
}

C. List POIs Attached to Route ​

  • Method & Path: GET /api/pois/route/:routeId
  • Response (200 OK):
typescript
{
  items: Array<Poi>
}

D. Delete POI ​

  • Method & Path: DELETE /api/pois/:id
  • Response (200 OK): { "success": true }

E. Bulk Delete POIs ​

  • Method & Path: POST /api/pois/bulk-delete
  • Request Body:
typescript
{
  ids: string[]; // Array of POI UUIDs
}
  • Response (200 OK): { "success": true }

🚲 4. Virtual Garage & Component Wear (/api/garage) ​

A. List Garage Bicycles ​

  • Method & Path: GET /api/garage
  • Response (200 OK):
typescript
{
  bikes: Array<{
    id: string;
    name: string;
    brand: string;
    model: string;
    type: string; // Road, Gravel, MTB, E-Bike
    stravaGearId: string | null;
    totalKm: number;
    isIgnored: boolean;
  }>
}

B. Get Components Wear Status ​

Fetches all mounted and inventory components with dynamic wear details.

  • Method & Path: GET /api/components
  • Response (200 OK):
typescript
{
  components: Array<{
    id: string;
    bikeId: string | null;
    name: string;
    category: "DRIVETRAIN" | "BRAKES" | "WHEELS" | "SUSPENSION" | "COCKPIT" | "ACCESSORIES";
    status: "INVENTORY" | "MOUNTED" | "ARCHIVED";
    
    // Dual-wear dynamic counters
    maxLifespanKm: number;
    currentLifespanKm: number;
    maxLifespanHours: number;
    currentLifespanHours: number;
    serviceIntervalKm: number;
    currentServiceKm: number;
    
    wearPercent: number; // Derived: (currentLifespanKm / maxLifespanKm) * 100
  }>
}

C. Log Component Service Actions ​

Logs component services (Clean, Lube, Replace) to reset active wear counters.

  • Method & Path: POST /api/components/service
  • Request Body:
typescript
{
  componentId: string;
  actionType: "clean" | "lube" | "replace" | "adjust";
  notes?: string;
  kmAtService: number;
}
  • Response (200 OK):
typescript
{
  success: true,
  logId: string,
  wearReset: boolean // True if "replace" action reset metrics to 0
}