Series Navigation: This is Part 16 of the 17-part API Development Series. Review Part 15: Testing & Contracts first.
API Development Mastery
Your 17-step learning path • Currently on Step 16
Backend API Fundamentals
REST, HTTP, status codes, URI designData Layer & Persistence
Database integration, CRUD, transactions, RedisOpenAPI Specification
Contract-first design, OpenAPI 3.0/3.1Documentation & DX
Swagger UI, Redoc, developer portalsAuthentication & Authorization
OAuth 2.0, JWT, RBAC, ABACSecurity Hardening
OWASP Top 10, input validation, CORSAWS API Gateway
REST/HTTP APIs, Lambda integration, WAFAzure API Management
Policies, products, developer portalGCP Apigee
API proxies, monetization, analyticsArchitecture Patterns
Gateway, BFF, microservices, DDDVersioning & Governance
SemVer, deprecation, lifecycleMonitoring & Analytics
Observability, tracing, SLIs/SLOsPerformance & Rate Limiting
Caching, throttling, load testingGraphQL & gRPC
Alternative API styles, Protocol BuffersTesting & Contracts
Contract testing, Pact, Postman/Newman16
CI/CD & Automation
Spectral, GitHub Actions, Terraform17
API Product Management
API as Product, monetization, ecosystemsAPI Linting with Spectral
What is Spectral?
Spectral is a flexible, JSON/YAML linter for OpenAPI specs. It enforces style guides and catches errors before they reach production.
# Install Spectral
npm install -g @stoplight/spectral-cli
# Lint OpenAPI spec
spectral lint openapi.yaml
# With custom ruleset
spectral lint openapi.yaml --ruleset .spectral.yaml
Custom Ruleset
# .spectral.yaml - Enterprise API standards
extends: spectral:oas
rules:
# Require descriptions
operation-description:
description: Operations must have descriptions
severity: warn
given: "$.paths.*[get,post,put,patch,delete]"
then:
field: description
function: truthy
# Enforce naming conventions
path-kebab-case:
description: Paths must be kebab-case
severity: error
given: "$.paths"
then:
field: "@key"
function: pattern
functionOptions:
match: "^(/[a-z0-9-{}]+)+$"
# Require security on all operations
operation-security:
description: All operations must define security
severity: error
given: "$.paths.*[get,post,put,patch,delete]"
then:
field: security
function: truthy
# Response body required for 2xx
response-body-required:
description: 2xx responses must have content
severity: warn
given: "$.paths.*.*.responses[?(@property >= 200 && @property < 300)]"
then:
field: content
function: truthy
# Contact info required
info-contact:
description: API must have contact info
severity: error
given: "$.info"
then:
field: contact
function: truthy
GitHub Actions Pipelines
Complete CI/CD Pipeline
# .github/workflows/api-pipeline.yml
name: API CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
AWS_REGION: us-east-1
jobs:
lint:
name: Lint & Validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint OpenAPI spec
run: npx spectral lint openapi.yaml --fail-severity=error
- name: Lint code
run: npm run lint
test:
name: Test
runs-on: ubuntu-latest
needs: lint
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run migrations
run: npm run db:migrate
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/testdb
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
contract-test:
name: Contract Tests
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Run Pact provider verification
run: npm run test:pact:provider
- name: Publish Pact results
run: npx pact-broker publish ./pacts --broker-base-url=${{ secrets.PACT_BROKER_URL }} --broker-token=${{ secrets.PACT_BROKER_TOKEN }}
if: github.ref == 'refs/heads/main'
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: [test, contract-test]
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy to staging
run: npm run deploy:staging
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [test, contract-test]
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy to production
run: npm run deploy:production
GitLab CI/CD
# .gitlab-ci.yml
stages:
- validate
- test
- build
- deploy
variables:
NODE_IMAGE: node:20-alpine
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
lint:
stage: validate
image: $NODE_IMAGE
script:
- npm ci
- npx spectral lint openapi.yaml --fail-severity=error
- npm run lint
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
test:
stage: test
image: $NODE_IMAGE
services:
- postgres:15
- redis:7
variables:
POSTGRES_PASSWORD: test
DATABASE_URL: postgres://postgres:test@postgres:5432/testdb
REDIS_URL: redis://redis:6379
script:
- npm ci
- npm run db:migrate
- npm run test:coverage
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy_production:
stage: deploy
image: alpine/k8s:1.28.0
environment:
name: production
url: https://api.example.com
script:
- kubectl set image deployment/api api=$DOCKER_IMAGE
- kubectl rollout status deployment/api
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
Blue/Green Deployments
Blue/Green Deployment: Run two identical production environments. Deploy to inactive (green), test, then switch traffic from active (blue) to green.
# AWS CodeDeploy appspec.yml for Blue/Green
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "arn:aws:ecs:region:account:task-definition/task-api"
LoadBalancerInfo:
ContainerName: "api"
ContainerPort: 3000
PlatformVersion: "LATEST"
Hooks:
- BeforeAllowTraffic: "BeforeAllowTrafficHook"
- AfterAllowTraffic: "AfterAllowTrafficHook"
Canary Releases
# Kubernetes canary with Istio
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: task-api
spec:
hosts:
- task-api
http:
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: task-api
subset: canary
- route:
- destination:
host: task-api
subset: stable
weight: 95
- destination:
host: task-api
subset: canary
weight: 5
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: task-api
spec:
host: task-api
subsets:
- name: stable
labels:
version: v1
- name: canary
labels:
version: v2
Terraform for APIs
# terraform/main.tf - API Infrastructure
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
backend "s3" {
bucket = "terraform-state-bucket"
key = "api/terraform.tfstate"
region = "us-east-1"
}
}
# API Gateway
resource "aws_apigatewayv2_api" "api" {
name = "task-api"
protocol_type = "HTTP"
cors_configuration {
allow_origins = var.allowed_origins
allow_methods = ["GET", "POST", "PUT", "DELETE"]
allow_headers = ["Content-Type", "Authorization"]
max_age = 300
}
}
resource "aws_apigatewayv2_stage" "prod" {
api_id = aws_apigatewayv2_api.api.id
name = "prod"
auto_deploy = true
access_log_settings {
destination_arn = aws_cloudwatch_log_group.api_logs.arn
format = jsonencode({
requestId = "$context.requestId"
ip = "$context.identity.sourceIp"
requestTime = "$context.requestTime"
httpMethod = "$context.httpMethod"
routeKey = "$context.routeKey"
status = "$context.status"
responseLatency = "$context.responseLatency"
})
}
}
# Lambda function
resource "aws_lambda_function" "api" {
filename = data.archive_file.lambda.output_path
function_name = "task-api"
role = aws_iam_role.lambda.arn
handler = "index.handler"
runtime = "nodejs20.x"
source_code_hash = data.archive_file.lambda.output_base64sha256
environment {
variables = {
DATABASE_URL = var.database_url
NODE_ENV = "production"
}
}
vpc_config {
subnet_ids = var.private_subnet_ids
security_group_ids = [aws_security_group.lambda.id]
}
}
# Integration
resource "aws_apigatewayv2_integration" "lambda" {
api_id = aws_apigatewayv2_api.api.id
integration_type = "AWS_PROXY"
integration_uri = aws_lambda_function.api.invoke_arn
payload_format_version = "2.0"
}
# Routes
resource "aws_apigatewayv2_route" "tasks" {
api_id = aws_apigatewayv2_api.api.id
route_key = "ANY /tasks/{proxy+}"
target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}
output "api_endpoint" {
value = aws_apigatewayv2_stage.prod.invoke_url
}
Practice Exercises
Exercise 1: Spectral Ruleset
- Create custom Spectral ruleset
- Add naming convention rules
- Integrate with pre-commit hooks
Exercise 2: GitHub Actions Pipeline
- Create multi-stage CI/CD pipeline
- Add linting, testing, and deployment
- Configure environment protection
Exercise 3: Terraform API Infrastructure
- Define API Gateway + Lambda with Terraform
- Add remote state and workspaces
- Implement blue/green with Terraform
Next Steps: In Part 17: API Product Management, we'll learn API as Product thinking, monetization strategies, and building developer ecosystems.