ADLenehan avatar

vercel-api

Patterns for programmatic deployment via Vercel API. Use this skill when implementing deployment wor

作者 ADLenehan|オープンソース

Vercel API Patterns

API Client Setup

const VERCEL_API = 'https://api.vercel.com'

interface VercelClientConfig {
  token: string
  teamId?: string
}

class VercelClient {
  constructor(private config: VercelClientConfig) {}

  private async request<T>(
    path: string,
    options: RequestInit = {}
  ): Promise<T> {
    const url = new URL(path, VERCEL_API)

    if (this.config.teamId) {
      url.searchParams.set('teamId', this.config.teamId)
    }

    const response = await fetch(url, {
      ...options,
      headers: {
        Authorization: `Bearer ${this.config.token}`,
        'Content-Type': 'application/json',
        ...options.headers,
      },
    })

    if (!response.ok) {
      const error = await response.json()
      throw new VercelAPIError(error.error.message, response.status)
    }

    return response.json()
  }
}

Project Creation

interface CreateProjectRequest {
  name: string
  framework?: 'nextjs' | 'vite' | 'remix' | null
  gitRepository?: {
    type: 'github' | 'gitlab' | 'bitbucket'
    repo: string
  }
  buildCommand?: string
  outputDirectory?: string
  rootDirectory?: string
}

async createProject(data: CreateProjectRequest) {
  return this.request<Project>('/v10/projects', {
    method: 'POST',
    body: JSON.stringify(data),
  })
}

Environment Variables

Set Environment Variables

interface EnvVariable {
  key: string
  value: string
  target: ('production' | 'preview' | 'development')[]
  type: 'plain' | 'secret' | 'encrypted'
}

async setEnvVariables(projectId: string, variables: EnvVariable[]) {
  return this.request(`/v10/projects/${projectId}/env`, {
    method: 'POST',
    body: JSON.stringify(variables),
  })
}

Get Environment Variables

async getEnvVariables(projectId: string) {
  return this.request<{ envs: EnvVariable[] }>(
    `/v9/projects/${projectId}/env`
  )
}

Deployment

Create Deployment from Files

interface DeploymentFile {
  file: string
  data: string // base64 encoded
}

async createDeployment(projectId: string, files: DeploymentFile[]) {
  return this.request<Deployment>('/v13/deployments', {
    method: 'POST',
    body: JSON.stringify({
      name: projectId,
      files,
      projectSettings: {
        framework: 'nextjs',
      },
    }),
  })
}

Check Deployment Status

type DeploymentState =
  | 'QUEUED'
  | 'BUILDING'
  | 'READY'
  | 'ERROR'
  | 'CANCELED'

async getDeployment(deploymentId: string) {
  return this.request<Deployment>(`/v13/deployments/${deploymentId}`)
}

async waitForDeployment(
  deploymentId: string,
  timeout = 300000
): Promise<Deployment> {
  const start = Date.now()

  while (Date.now() - start < timeout) {
    const deployment = await this.getDeployment(deploymentId)

    if (deployment.readyState === 'READY') {
      return deployment
    }

    if (deployment.readyState === 'ERROR') {
      throw new Error(`Deployment failed: ${deployment.errorMessage}`)
    }

    await new Promise((r) => setTimeout(r, 2000))
  }

  throw new Error('Deployment timeout')
}

Domain Configuration

Add Domain

async addDomain(projectId: string, domain: string) {
  return this.request(`/v10/projects/${projectId}/domains`, {
    method: 'POST',
    body: JSON.stringify({ name: domain }),
  })
}

Configure Subdomain

Battery uses subdomains for deployed apps: {app-name}.{org}.battery.app

async configureBatteryDomain(
  projectId: string,
  appName: string,
  orgSlug: string
) {
  const domain = `${appName}.${orgSlug}.battery.app`
  return this.addDomain(projectId, domain)
}

Error Handling

class VercelAPIError extends Error {
  constructor(
    message: string,
    public statusCode: number
  ) {
    super(message)
    this.name = 'VercelAPIError'
  }
}

// Handle common errors
try {
  await client.createProject({ name: 'my-app' })
} catch (error) {
  if (error instanceof VercelAPIError) {
    switch (error.statusCode) {
      case 400:
        // Invalid request
        break
      case 401:
        // Invalid token
        break
      case 403:
        // Insufficient permissions
        break
      case 409:
        // Project already exists
        break
    }
  }
}

Rate Limiting

Vercel API has rate limits. Implement backoff:

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3
): Promise<T> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn()
    } catch (error) {
      if (
        error instanceof VercelAPIError &&
        error.statusCode === 429 &&
        i < maxRetries - 1
      ) {
        await new Promise((r) => setTimeout(r, 2 ** i * 1000))
        continue
      }
      throw error
    }
  }
  throw new Error('Max retries exceeded')
}

Key Endpoints Reference

OperationMethodEndpoint
List projectsGET/v9/projects
Create projectPOST/v10/projects
Get projectGET/v9/projects/{idOrName}
Delete projectDELETE/v9/projects/{idOrName}
Create deploymentPOST/v13/deployments
Get deploymentGET/v13/deployments/{id}
List deploymentsGET/v6/deployments
Set env varsPOST/v10/projects/{id}/env
Add domainPOST/v10/projects/{id}/domains