Claude Code Plugins

Community-maintained marketplace

Feedback

construction-api-developer

@CBoser/ConstructionPlatform
0
0

Specialized skill for developing RESTful APIs for the construction project management platform. Use when building backend services for job management, plan specifications (40 plans, 567 options), PDSS integration, schedule coordination, contract/PO tracking, or subdivision management (31 subdivisions). Includes API design patterns, data models, business logic, validation rules, and integration endpoints specific to construction workflows.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name construction-api-developer
description Specialized skill for developing RESTful APIs for the construction project management platform. Use when building backend services for job management, plan specifications (40 plans, 567 options), PDSS integration, schedule coordination, contract/PO tracking, or subdivision management (31 subdivisions). Includes API design patterns, data models, business logic, validation rules, and integration endpoints specific to construction workflows.

Construction API Developer

Comprehensive guide for building the backend API services for the integrated construction project management platform.

API Architecture Overview

Technology Stack

  • Runtime: Node.js 18+ with Express.js
  • Database: PostgreSQL 14+ with Sequelize ORM
  • Cache: Redis for session and query caching
  • Authentication: JWT with role-based access control (RBAC)
  • Real-time: Socket.io for live updates
  • Documentation: OpenAPI 3.0 / Swagger
  • Testing: Jest + Supertest

Project Structure

/api
├── /src
│   ├── /controllers     # Request handlers
│   ├── /models          # Database models
│   ├── /services        # Business logic
│   ├── /middleware      # Auth, validation, logging
│   ├── /routes          # API route definitions
│   ├── /validators      # Request validation schemas
│   ├── /utils           # Helper functions
│   └── /config          # Configuration files
├── /tests
│   ├── /unit
│   ├── /integration
│   └── /fixtures
└── server.js

Core Data Models

1. Plan Model

// models/Plan.js
const { DataTypes } = require('sequelize');

module.exports = (sequelize) => {
  const Plan = sequelize.define('Plan', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    planCode: {
      type: DataTypes.STRING(20),
      unique: true,
      allowNull: false,
      validate: {
        is: /^PL-\d{4}$/  // Format: PL-1234
      }
    },
    planName: {
      type: DataTypes.STRING(100),
      allowNull: false
    },
    baseSquareFootage: {
      type: DataTypes.INTEGER,
      allowNull: false
    },
    bedrooms: {
      type: DataTypes.INTEGER,
      allowNull: false
    },
    bathrooms: {
      type: DataTypes.DECIMAL(3, 1),
      allowNull: false
    },
    stories: {
      type: DataTypes.INTEGER,
      allowNull: false
    },
    garageType: {
      type: DataTypes.ENUM('attached', 'detached', 'none'),
      defaultValue: 'attached'
    },
    availableElevations: {
      type: DataTypes.ARRAY(DataTypes.STRING),
      defaultValue: []
    },
    basePriceTier: {
      type: DataTypes.STRING(20)
    },
    status: {
      type: DataTypes.ENUM('active', 'inactive', 'archived'),
      defaultValue: 'active'
    },
    thumbnailUrl: {
      type: DataTypes.STRING(500)
    },
    floorPlanUrl: {
      type: DataTypes.STRING(500)
    },
    marketingDescription: {
      type: DataTypes.TEXT
    }
  }, {
    timestamps: true,
    indexes: [
      { fields: ['planCode'] },
      { fields: ['status'] },
      { fields: ['bedrooms', 'bathrooms'] }
    ]
  });

  Plan.associate = (models) => {
    Plan.hasMany(models.PlanOption, { foreignKey: 'planId', as: 'options' });
    Plan.hasMany(models.Job, { foreignKey: 'planId', as: 'jobs' });
  };

  return Plan;
};

2. PlanOption Model

// models/PlanOption.js
module.exports = (sequelize) => {
  const PlanOption = sequelize.define('PlanOption', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    optionCode: {
      type: DataTypes.STRING(30),
      unique: true,
      allowNull: false
    },
    optionName: {
      type: DataTypes.STRING(150),
      allowNull: false
    },
    category: {
      type: DataTypes.ENUM(
        'exterior',
        'interior',
        'structural',
        'mechanical',
        'electrical',
        'plumbing',
        'hvac',
        'flooring',
        'cabinets',
        'countertops',
        'fixtures',
        'other'
      ),
      allowNull: false
    },
    description: {
      type: DataTypes.TEXT
    },
    priceTier: {
      type: DataTypes.STRING(20)
    },
    availableElevations: {
      type: DataTypes.ARRAY(DataTypes.STRING),
      defaultValue: []
    },
    compatibleOptions: {
      type: DataTypes.ARRAY(DataTypes.STRING),
      defaultValue: [],
      comment: 'Array of optionCode values this option works with'
    },
    conflictingOptions: {
      type: DataTypes.ARRAY(DataTypes.STRING),
      defaultValue: [],
      comment: 'Array of optionCode values this option conflicts with'
    },
    requiredOptions: {
      type: DataTypes.ARRAY(DataTypes.STRING),
      defaultValue: [],
      comment: 'Array of optionCode values required before selecting this'
    },
    leadTimeDays: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      comment: 'Additional lead time this option adds'
    },
    impactsSchedule: {
      type: DataTypes.BOOLEAN,
      defaultValue: false
    },
    requiresEngineering: {
      type: DataTypes.BOOLEAN,
      defaultValue: false
    },
    status: {
      type: DataTypes.ENUM('available', 'discontinued', 'seasonal'),
      defaultValue: 'available'
    }
  }, {
    timestamps: true,
    indexes: [
      { fields: ['optionCode'] },
      { fields: ['category'] },
      { fields: ['status'] }
    ]
  });

  PlanOption.associate = (models) => {
    PlanOption.belongsTo(models.Plan, { foreignKey: 'planId', as: 'plan' });
    PlanOption.hasMany(models.JobOption, { foreignKey: 'optionId', as: 'jobSelections' });
  };

  return PlanOption;
};

3. Job Model

// models/Job.js
module.exports = (sequelize) => {
  const Job = sequelize.define('Job', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    jobNumber: {
      type: DataTypes.STRING(50),
      unique: true,
      allowNull: false
    },
    lotNumber: {
      type: DataTypes.STRING(50),
      allowNull: false
    },
    streetAddress: {
      type: DataTypes.STRING(200)
    },
    clientName: {
      type: DataTypes.STRING(150)
    },
    clientEmail: {
      type: DataTypes.STRING(150)
    },
    clientPhone: {
      type: DataTypes.STRING(20)
    },
    elevation: {
      type: DataTypes.STRING(50)
    },
    status: {
      type: DataTypes.ENUM(
        'plan_start',
        'pdss_initiated',
        'plan_review',
        'approved',
        'in_production',
        'quality_check',
        'completed',
        'on_hold',
        'cancelled'
      ),
      defaultValue: 'plan_start'
    },
    priority: {
      type: DataTypes.ENUM('low', 'medium', 'high', 'urgent'),
      defaultValue: 'medium'
    },
    estimatedStartDate: {
      type: DataTypes.DATE
    },
    actualStartDate: {
      type: DataTypes.DATE
    },
    estimatedCompletionDate: {
      type: DataTypes.DATE
    },
    actualCompletionDate: {
      type: DataTypes.DATE
    },
    notes: {
      type: DataTypes.TEXT
    },
    flags: {
      type: DataTypes.ARRAY(DataTypes.STRING),
      defaultValue: [],
      comment: 'Issue flags like "missing_docs", "engineering_review", etc.'
    },
    metadata: {
      type: DataTypes.JSONB,
      defaultValue: {},
      comment: 'Flexible field for additional data'
    }
  }, {
    timestamps: true,
    paranoid: true,  // Soft delete support
    indexes: [
      { fields: ['jobNumber'] },
      { fields: ['status'] },
      { fields: ['subdivisionId'] },
      { fields: ['planId'] },
      { fields: ['estimatedStartDate'] }
    ]
  });

  Job.associate = (models) => {
    Job.belongsTo(models.Plan, { foreignKey: 'planId', as: 'plan' });
    Job.belongsTo(models.Subdivision, { foreignKey: 'subdivisionId', as: 'subdivision' });
    Job.hasMany(models.JobOption, { foreignKey: 'jobId', as: 'selectedOptions' });
    Job.hasMany(models.TimeEntry, { foreignKey: 'jobId', as: 'timeEntries' });
    Job.hasOne(models.Contract, { foreignKey: 'jobId', as: 'contract' });
    Job.hasMany(models.PDSSDocument, { foreignKey: 'jobId', as: 'pdssDocuments' });
  };

  return Job;
};

4. Subdivision Model

// models/Subdivision.js
module.exports = (sequelize) => {
  const Subdivision = sequelize.define('Subdivision', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    subdivisionCode: {
      type: DataTypes.STRING(30),
      unique: true,
      allowNull: false
    },
    subdivisionName: {
      type: DataTypes.STRING(150),
      allowNull: false
    },
    builderName: {
      type: DataTypes.STRING(150),
      allowNull: false
    },
    city: {
      type: DataTypes.STRING(100)
    },
    state: {
      type: DataTypes.STRING(2)
    },
    zipCode: {
      type: DataTypes.STRING(10)
    },
    totalLots: {
      type: DataTypes.INTEGER
    },
    availablePlans: {
      type: DataTypes.ARRAY(DataTypes.STRING),
      defaultValue: [],
      comment: 'Array of planCode values available in this subdivision'
    },
    specialRequirements: {
      type: DataTypes.TEXT
    },
    hoa: {
      type: DataTypes.BOOLEAN,
      defaultValue: false
    },
    status: {
      type: DataTypes.ENUM('active', 'sold_out', 'inactive'),
      defaultValue: 'active'
    },
    contactName: {
      type: DataTypes.STRING(150)
    },
    contactEmail: {
      type: DataTypes.STRING(150)
    },
    contactPhone: {
      type: DataTypes.STRING(20)
    }
  }, {
    timestamps: true,
    indexes: [
      { fields: ['subdivisionCode'] },
      { fields: ['status'] },
      { fields: ['builderName'] }
    ]
  });

  Subdivision.associate = (models) => {
    Subdivision.hasMany(models.Job, { foreignKey: 'subdivisionId', as: 'jobs' });
  };

  return Subdivision;
};

API Endpoints Structure

Plan Management API

// routes/plans.js
const express = require('express');
const router = express.Router();
const PlanController = require('../controllers/PlanController');
const { authenticate, authorize } = require('../middleware/auth');
const { validatePlan } = require('../validators/planValidator');

/**
 * @route   GET /api/v1/plans
 * @desc    Get all plans with filtering and pagination
 * @access  Public
 */
router.get('/', PlanController.getPlans);

/**
 * @route   GET /api/v1/plans/:id
 * @desc    Get plan by ID with all options
 * @access  Public
 */
router.get('/:id', PlanController.getPlanById);

/**
 * @route   GET /api/v1/plans/:id/options
 * @desc    Get all options for a specific plan
 * @access  Public
 */
router.get('/:id/options', PlanController.getPlanOptions);

/**
 * @route   POST /api/v1/plans
 * @desc    Create new plan
 * @access  Private (Admin only)
 */
router.post('/', 
  authenticate, 
  authorize(['admin']), 
  validatePlan, 
  PlanController.createPlan
);

/**
 * @route   PUT /api/v1/plans/:id
 * @desc    Update plan
 * @access  Private (Admin only)
 */
router.put('/:id', 
  authenticate, 
  authorize(['admin']), 
  validatePlan, 
  PlanController.updatePlan
);

/**
 * @route   DELETE /api/v1/plans/:id
 * @desc    Archive plan (soft delete)
 * @access  Private (Admin only)
 */
router.delete('/:id', 
  authenticate, 
  authorize(['admin']), 
  PlanController.archivePlan
);

/**
 * @route   POST /api/v1/plans/:id/validate-options
 * @desc    Validate option selection for compatibility
 * @access  Public
 */
router.post('/:id/validate-options', PlanController.validateOptionSelection);

module.exports = router;

Plan Controller Implementation

// controllers/PlanController.js
const PlanService = require('../services/PlanService');
const { validationResult } = require('express-validator');

class PlanController {
  /**
   * Get all plans with filtering and pagination
   */
  static async getPlans(req, res) {
    try {
      const { 
        page = 1, 
        limit = 20, 
        status = 'active',
        bedrooms,
        bathrooms,
        stories,
        search
      } = req.query;

      const filters = {
        status,
        ...(bedrooms && { bedrooms: parseInt(bedrooms) }),
        ...(bathrooms && { bathrooms: parseFloat(bathrooms) }),
        ...(stories && { stories: parseInt(stories) }),
        ...(search && { search })
      };

      const result = await PlanService.getPlans(filters, {
        page: parseInt(page),
        limit: parseInt(limit)
      });

      res.json({
        success: true,
        data: result.plans,
        pagination: {
          currentPage: result.currentPage,
          totalPages: result.totalPages,
          totalRecords: result.totalRecords,
          limit: result.limit
        }
      });
    } catch (error) {
      console.error('Error in getPlans:', error);
      res.status(500).json({
        success: false,
        message: 'Error retrieving plans',
        error: error.message
      });
    }
  }

  /**
   * Get plan by ID with options
   */
  static async getPlanById(req, res) {
    try {
      const { id } = req.params;
      const { includeOptions = true } = req.query;

      const plan = await PlanService.getPlanById(id, includeOptions);

      if (!plan) {
        return res.status(404).json({
          success: false,
          message: 'Plan not found'
        });
      }

      res.json({
        success: true,
        data: plan
      });
    } catch (error) {
      console.error('Error in getPlanById:', error);
      res.status(500).json({
        success: false,
        message: 'Error retrieving plan',
        error: error.message
      });
    }
  }

  /**
   * Validate option selection for conflicts and requirements
   */
  static async validateOptionSelection(req, res) {
    try {
      const { id } = req.params;
      const { selectedOptions, elevation } = req.body;

      const validation = await PlanService.validateOptions(
        id,
        selectedOptions,
        elevation
      );

      res.json({
        success: true,
        data: validation
      });
    } catch (error) {
      console.error('Error in validateOptionSelection:', error);
      res.status(500).json({
        success: false,
        message: 'Error validating options',
        error: error.message
      });
    }
  }
}

module.exports = PlanController;

Business Logic Service

// services/PlanService.js
const { Plan, PlanOption } = require('../models');
const { Op } = require('sequelize');

class PlanService {
  /**
   * Validate option selection against business rules
   * - Check for conflicting options
   * - Verify required dependencies
   * - Ensure elevation compatibility
   */
  static async validateOptions(planId, selectedOptionCodes, elevation) {
    const plan = await Plan.findByPk(planId, {
      include: [{ model: PlanOption, as: 'options' }]
    });

    if (!plan) {
      throw new Error('Plan not found');
    }

    const validation = {
      isValid: true,
      errors: [],
      warnings: [],
      recommendations: []
    };

    // Get full option objects for selected codes
    const selectedOptions = plan.options.filter(opt => 
      selectedOptionCodes.includes(opt.optionCode)
    );

    // Check elevation compatibility
    if (elevation) {
      const incompatibleOptions = selectedOptions.filter(opt => 
        opt.availableElevations.length > 0 && 
        !opt.availableElevations.includes(elevation)
      );

      if (incompatibleOptions.length > 0) {
        validation.isValid = false;
        validation.errors.push({
          type: 'ELEVATION_INCOMPATIBLE',
          message: `Options not available for elevation ${elevation}`,
          optionCodes: incompatibleOptions.map(opt => opt.optionCode)
        });
      }
    }

    // Check for conflicts
    for (const option of selectedOptions) {
      const conflicts = option.conflictingOptions.filter(code => 
        selectedOptionCodes.includes(code)
      );

      if (conflicts.length > 0) {
        validation.isValid = false;
        validation.errors.push({
          type: 'OPTION_CONFLICT',
          message: `${option.optionName} conflicts with selected options`,
          optionCode: option.optionCode,
          conflictsWith: conflicts
        });
      }
    }

    // Check for missing requirements
    for (const option of selectedOptions) {
      const missingRequirements = option.requiredOptions.filter(code =>
        !selectedOptionCodes.includes(code)
      );

      if (missingRequirements.length > 0) {
        validation.isValid = false;
        validation.errors.push({
          type: 'MISSING_REQUIREMENT',
          message: `${option.optionName} requires additional options`,
          optionCode: option.optionCode,
          requires: missingRequirements
        });
      }
    }

    // Generate recommendations for compatible options
    const allCompatibleCodes = selectedOptions
      .flatMap(opt => opt.compatibleOptions)
      .filter(code => !selectedOptionCodes.includes(code));

    const uniqueCompatible = [...new Set(allCompatibleCodes)];
    
    if (uniqueCompatible.length > 0) {
      const compatibleOptions = await PlanOption.findAll({
        where: { optionCode: uniqueCompatible }
      });

      validation.recommendations = compatibleOptions.map(opt => ({
        optionCode: opt.optionCode,
        optionName: opt.optionName,
        category: opt.category,
        reason: 'Frequently selected with your current choices'
      }));
    }

    // Check for schedule impacts
    const scheduleImpact = selectedOptions
      .filter(opt => opt.impactsSchedule)
      .reduce((sum, opt) => sum + opt.leadTimeDays, 0);

    if (scheduleImpact > 14) {
      validation.warnings.push({
        type: 'SCHEDULE_IMPACT',
        message: `Selected options add ${scheduleImpact} days to schedule`,
        additionalDays: scheduleImpact
      });
    }

    // Check for engineering requirements
    const needsEngineering = selectedOptions.some(opt => opt.requiresEngineering);
    if (needsEngineering) {
      validation.warnings.push({
        type: 'ENGINEERING_REQUIRED',
        message: 'One or more options require engineering review',
        optionCodes: selectedOptions
          .filter(opt => opt.requiresEngineering)
          .map(opt => opt.optionCode)
      });
    }

    return validation;
  }
}

module.exports = PlanService;

Authentication & Authorization

JWT Middleware

// middleware/auth.js
const jwt = require('jsonwebtoken');
const { User } = require('../models');

/**
 * Authenticate JWT token
 */
const authenticate = async (req, res, next) => {
  try {
    const token = req.header('Authorization')?.replace('Bearer ', '');

    if (!token) {
      return res.status(401).json({
        success: false,
        message: 'Access denied. No token provided.'
      });
    }

    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await User.findByPk(decoded.userId);

    if (!user) {
      return res.status(401).json({
        success: false,
        message: 'Invalid token. User not found.'
      });
    }

    req.user = user;
    next();
  } catch (error) {
    res.status(401).json({
      success: false,
      message: 'Invalid token.',
      error: error.message
    });
  }
};

/**
 * Authorize based on roles
 */
const authorize = (roles = []) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({
        success: false,
        message: 'Unauthorized'
      });
    }

    if (roles.length && !roles.includes(req.user.role)) {
      return res.status(403).json({
        success: false,
        message: 'Forbidden. Insufficient permissions.'
      });
    }

    next();
  };
};

module.exports = { authenticate, authorize };

Testing Strategy

Unit Test Example

// tests/unit/services/PlanService.test.js
const PlanService = require('../../../src/services/PlanService');
const { Plan, PlanOption } = require('../../../src/models');

jest.mock('../../../src/models');

describe('PlanService.validateOptions', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should detect conflicting options', async () => {
    const mockPlan = {
      id: '123',
      options: [
        {
          optionCode: 'OPT-001',
          optionName: 'Hardwood Floors',
          conflictingOptions: ['OPT-002'],
          requiredOptions: [],
          availableElevations: []
        },
        {
          optionCode: 'OPT-002',
          optionName: 'Carpet',
          conflictingOptions: ['OPT-001'],
          requiredOptions: [],
          availableElevations: []
        }
      ]
    };

    Plan.findByPk.mockResolvedValue(mockPlan);

    const result = await PlanService.validateOptions(
      '123',
      ['OPT-001', 'OPT-002'],
      'A'
    );

    expect(result.isValid).toBe(false);
    expect(result.errors).toHaveLength(2);
    expect(result.errors[0].type).toBe('OPTION_CONFLICT');
  });

  it('should detect missing requirements', async () => {
    const mockPlan = {
      id: '123',
      options: [
        {
          optionCode: 'OPT-003',
          optionName: 'Upgraded Electrical',
          conflictingOptions: [],
          requiredOptions: ['OPT-004'],  // Requires breaker panel upgrade
          availableElevations: []
        }
      ]
    };

    Plan.findByPk.mockResolvedValue(mockPlan);

    const result = await PlanService.validateOptions(
      '123',
      ['OPT-003'],
      'A'
    );

    expect(result.isValid).toBe(false);
    expect(result.errors[0].type).toBe('MISSING_REQUIREMENT');
  });
});

Performance Optimization

Caching Strategy

// utils/cache.js
const Redis = require('redis');
const client = Redis.createClient({ url: process.env.REDIS_URL });

client.connect();

/**
 * Cache wrapper for frequently accessed data
 */
const cacheWrapper = (key, ttl = 3600) => {
  return async (fetchFunction) => {
    try {
      // Try to get from cache
      const cached = await client.get(key);
      if (cached) {
        return JSON.parse(cached);
      }

      // Fetch from database
      const data = await fetchFunction();

      // Store in cache
      await client.setEx(key, ttl, JSON.stringify(data));

      return data;
    } catch (error) {
      console.error('Cache error:', error);
      // Fallback to direct fetch if cache fails
      return await fetchFunction();
    }
  };
};

// Usage in service
const getPlanWithCache = async (planId) => {
  return cacheWrapper(`plan:${planId}`, 1800)(async () => {
    return await Plan.findByPk(planId, {
      include: [{ model: PlanOption, as: 'options' }]
    });
  });
};

module.exports = { cacheWrapper, client };

API Documentation Standards

Every endpoint must include:

  • Clear description
  • Request parameters with types
  • Request body schema
  • Response codes and schemas
  • Example requests/responses
  • Authentication requirements

Use Swagger/OpenAPI for interactive documentation.

Error Handling Standards

// utils/ApiError.js
class ApiError extends Error {
  constructor(statusCode, message, errors = []) {
    super(message);
    this.statusCode = statusCode;
    this.errors = errors;
    this.isOperational = true;
  }

  static badRequest(message, errors = []) {
    return new ApiError(400, message, errors);
  }

  static unauthorized(message = 'Unauthorized') {
    return new ApiError(401, message);
  }

  static forbidden(message = 'Forbidden') {
    return new ApiError(403, message);
  }

  static notFound(message = 'Resource not found') {
    return new ApiError(404, message);
  }

  static internal(message = 'Internal server error') {
    return new ApiError(500, message);
  }
}

module.exports = ApiError;

Environment Configuration

// config/config.js
require('dotenv').config();

module.exports = {
  development: {
    database: process.env.DB_NAME,
    username: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    host: process.env.DB_HOST,
    dialect: 'postgres',
    logging: console.log,
    pool: {
      max: 5,
      min: 0,
      acquire: 30000,
      idle: 10000
    }
  },
  production: {
    database: process.env.DB_NAME,
    username: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    host: process.env.DB_HOST,
    dialect: 'postgres',
    logging: false,
    pool: {
      max: 20,
      min: 5,
      acquire: 30000,
      idle: 10000
    }
  }
};

Key Metrics to Monitor

Performance Metrics

  • API response time (target: <500ms)
  • Database query time (target: <100ms)
  • Cache hit rate (target: >80%)
  • Throughput (requests per second)

Business Metrics

  • Job creation rate
  • Option validation requests
  • Most popular plan/option combinations
  • Error rates by endpoint

Infrastructure Metrics

  • CPU usage
  • Memory usage
  • Database connections
  • Redis memory usage