Introduction
Code formatting is essential for maintaining consistent, readable, and maintainable code. However, integrating formatters directly into applications or CI/CD pipelines can be challenging, especially when dealing with multiple languages and formatting tools. Building a serverless formatting API with AWS Lambda offers a flexible, scalable solution that can be used across projects and teams.
This guide walks through creating a serverless code formatting API using AWS Lambda and API Gateway. The resulting service can handle formatting requests for multiple programming languages, scale automatically with demand, and operate with minimal maintenance.
Benefits of Serverless Formatting APIs
Before diving into implementation details, let's understand why a serverless approach is ideal for code formatting:
- No server management: AWS handles the underlying infrastructure
- Pay-per-use pricing: Only pay for the actual formatting requests processed
- Automatic scaling: Handles traffic spikes without provisioning additional resources
- Language flexibility: Support multiple programming languages in one unified API
- High availability: AWS's infrastructure ensures reliable service
- Integration friendly: Easily consumed by web applications, IDEs, and CI/CD pipelines
Architecture Overview
Our serverless formatting API will consist of these main components:
- API Gateway: Manages API endpoints, request/response handling, and authentication
- Lambda Functions: Execute code formatting logic based on the programming language
- Formatters: Libraries like Prettier, Black, or gofmt packaged with Lambda functions
- CloudWatch: Monitors performance and logs errors
- IAM Roles: Manages permissions between services
The typical request flow works like this:
- Client sends unformatted code to a specific endpoint (e.g., /format/javascript)
- API Gateway routes the request to the appropriate Lambda function
- Lambda executes the formatting logic for the specified language
- Formatted code is returned to the client
Prerequisites
Before you begin, ensure you have:
- An AWS account with appropriate permissions
- AWS CLI installed and configured
- Node.js (v14 or later) for Lambda development
- Basic knowledge of JavaScript/TypeScript
- Familiarity with code formatters (like Prettier)
# Install AWS CLI
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
# Configure AWS CLI
aws configure
# Install Serverless Framework (optional but helpful)
npm install -g serverless
Setting Up AWS Lambda
Let's start by creating our Lambda function for JavaScript code formatting with Prettier.
Creating the Lambda Function
First, set up a new project folder and initialize it:
mkdir code-formatter-api
cd code-formatter-api
npm init -y
npm install prettier @types/aws-lambda aws-sdk
Create a TypeScript file for our JavaScript formatter Lambda function:
// src/formatters/javascript.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import * as prettier from 'prettier';
export const handler = async (event: APIGatewayProxyEvent): Promise => {
try {
// Parse the request body
const requestBody = JSON.parse(event.body || '{}');
const { code, options = {} } = requestBody;
if (!code) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'No code provided' })
};
}
// Default Prettier options with user overrides
const prettierOptions = {
parser: 'babel',
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: false,
trailingComma: 'es5',
bracketSpacing: true,
...options
};
// Format the code
const formattedCode = prettier.format(code, prettierOptions);
// Return the formatted code
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*', // For CORS support
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
},
body: JSON.stringify({
formatted: formattedCode,
language: 'javascript'
})
};
} catch (error) {
console.error('Error formatting code:', error);
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Failed to format code',
details: error instanceof Error ? error.message : String(error)
})
};
}
};
Managing Dependencies
AWS Lambda has size limits, so we need to be careful with dependencies. Create a webpack configuration to bundle our code:
npm install --save-dev webpack webpack-cli typescript ts-loader
npm install --save-dev serverless-webpack @types/node
Create a webpack configuration file:
// webpack.config.js
const path = require('path');
const slsw = require('serverless-webpack');
module.exports = {
mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
entry: slsw.lib.entries,
devtool: 'source-map',
resolve: {
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
},
output: {
libraryTarget: 'commonjs',
path: path.join(__dirname, '.webpack'),
filename: '[name].js',
},
target: 'node',
module: {
rules: [
{ test: /\.tsx?$/, loader: 'ts-loader' },
],
},
};
Create a tsconfig.json file:
{
"compilerOptions": {
"target": "es2019",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"sourceMap": true,
"outDir": "dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Configuring API Gateway
Next, we'll set up API Gateway to expose our Lambda function as a REST API.
Creating API Endpoints
We'll use the Serverless Framework to simplify deployment. Create a serverless.yml file:
# serverless.yml
service: code-formatter-api
provider:
name: aws
runtime: nodejs14.x
region: us-east-1
memorySize: 256
timeout: 10
stage: \${opt:stage, 'dev'}
lambdaHashingVersion: 20201221
# IAM permissions
iamRoleStatements:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "arn:aws:logs:*:*:*"
functions:
formatJavascript:
handler: src/formatters/javascript.handler
events:
- http:
path: format/javascript
method: post
cors: true
- http:
path: format/javascript
method: options
cors: true
plugins:
- serverless-webpack
custom:
webpack:
webpackConfig: webpack.config.js
includeModules: true
Implementing Authentication
For production APIs, you should implement authentication. Here's how to set up API keys:
# Add to serverless.yml
provider:
# ...existing configuration
apiGateway:
apiKeys:
- name: formatter-api-key-\${self:provider.stage}
description: API key for the code formatter service
enabled: true
functions:
formatJavascript:
# ...existing configuration
events:
- http:
path: format/javascript
method: post
cors: true
private: true # Requires API key
# ...other events
For more sophisticated authentication, consider using AWS Cognito or custom Lambda authorizers.
Implementing Code Formatting
Let's expand our API to support more programming languages and formatting options.
Using Prettier
Prettier is versatile and can format multiple languages. Let's create a more comprehensive formatter:
// src/formatters/prettier-formatter.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import * as prettier from 'prettier';
export const handler = async (event: APIGatewayProxyEvent): Promise => {
try {
// Parse the request body
const requestBody = JSON.parse(event.body || '{}');
const { code, language = 'javascript', options = {} } = requestBody;
if (!code) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'No code provided' })
};
}
// Map language to Prettier parser
const parserMap: Record = {
'javascript': 'babel',
'typescript': 'typescript',
'css': 'css',
'html': 'html',
'json': 'json',
'markdown': 'markdown',
'yaml': 'yaml'
};
const parser = parserMap[language] || parserMap.javascript;
// Default Prettier options with user overrides
const prettierOptions = {
parser,
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: false,
...options
};
// Format the code
const formattedCode = prettier.format(code, prettierOptions);
// Return the formatted code
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type,X-Api-Key',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
},
body: JSON.stringify({
formatted: formattedCode,
language,
parser
})
};
} catch (error) {
console.error('Error formatting code:', error);
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Failed to format code',
details: error instanceof Error ? error.message : String(error)
})
};
}
};
Update your serverless.yml to include this new function:
functions:
# Original function
formatJavascript:
handler: src/formatters/javascript.handler
events:
- http:
path: format/javascript
method: post
cors: true
private: true
- http:
path: format/javascript
method: options
cors: true
# New multi-language formatter
formatWithPrettier:
handler: src/formatters/prettier-formatter.handler
events:
- http:
path: format
method: post
cors: true
private: true
- http:
path: format
method: options
cors: true
Supporting Multiple Languages
For languages not supported by Prettier, you can create separate Lambda functions using other formatters:
// src/formatters/python.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { spawn } from 'child_process';
import { writeFileSync, readFileSync, unlinkSync } from 'fs';
import { join } from 'path';
// Using Python Black formatter with Lambda Layers
export const handler = async (event: APIGatewayProxyEvent): Promise => {
try {
const requestBody = JSON.parse(event.body || '{}');
const { code } = requestBody;
if (!code) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'No code provided' })
};
}
// Using Lambda's /tmp directory for temp files
const inputFile = join('/tmp', 'input.py');
const outputFile = join('/tmp', 'output.py');
// Write input code to file
writeFileSync(inputFile, code);
// Format using Black (assuming Black is installed in a Lambda Layer)
await new Promise((resolve, reject) => {
const black = spawn('/opt/python/bin/black', [
'--quiet',
inputFile,
'-o',
outputFile
]);
black.on('close', (code) => {
if (code === 0) {
resolve(true);
} else {
reject(new Error(`Black exited with code \${code}`));
}
});
});
// Read formatted code
const formattedCode = readFileSync(outputFile, 'utf8');
// Clean up temp files
unlinkSync(inputFile);
unlinkSync(outputFile);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type,X-Api-Key',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
},
body: JSON.stringify({
formatted: formattedCode,
language: 'python'
})
};
} catch (error) {
console.error('Error formatting Python code:', error);
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Failed to format Python code',
details: error instanceof Error ? error.message : String(error)
})
};
}
};
For this to work, you'd need to create a Lambda Layer with the Python Black formatter. This approach demonstrates how you can extend your API to support any language with available formatters.
Deployment
Deploy your serverless application using the Serverless Framework:
# Install dependencies
npm install
# Deploy to AWS
npx serverless deploy
# Deploy to a specific stage
npx serverless deploy --stage production
After deployment, you'll receive output with your API endpoints and API keys.
Testing Your API
Test your formatting API using curl or Postman:
curl -X POST \\
https://your-api-id.execute-api.us-east-1.amazonaws.com/dev/format \\
-H 'Content-Type: application/json' \\
-H 'x-api-key: your-api-key' \\
-d '{
"code": "function hello(name) {return \\"Hello, \\" + name;}",
"language": "javascript",
"options": {
"singleQuote": true,
"semi": false
}
}'
You should receive a response with the formatted code:
{
"formatted": "function hello(name) {\\n return 'Hello, ' + name\\n}\\n",
"language": "javascript",
"parser": "babel"
}
Monitoring and Logging
AWS provides several options for monitoring your Lambda functions and API Gateway:
- CloudWatch Logs: Automatically captures logs from your Lambda functions
- CloudWatch Metrics: Tracks invocation count, duration, error rates, etc.
- X-Ray: Provides tracing information to help debug performance issues
Add structured logging to your Lambda functions:
// Add to your Lambda handlers
console.log(JSON.stringify({
level: 'info',
message: 'Formatting request received',
language,
codeLength: code.length,
timestamp: new Date().toISOString()
}));
To enable X-Ray tracing, update your serverless.yml:
provider:
# ...existing configuration
tracing:
apiGateway: true
lambda: true
Scaling and Performance
AWS Lambda scales automatically, but there are optimizations you can make:
- Memory allocation: Increase memory (which also increases CPU) for faster formatting of large files
- Provisioned concurrency: Eliminate cold starts for production APIs
- Timeout configuration: Set appropriate timeouts based on expected code size
- Reuse connections: Implement connection pooling for any database or external services
Configure provisioned concurrency in serverless.yml:
functions:
formatWithPrettier:
handler: src/formatters/prettier-formatter.handler
provisionedConcurrency: 5 # Keep 5 instances warm
# ...rest of configuration
Cost Optimization
AWS Lambda costs are based on:
- Number of requests
- Duration of execution
- Memory allocated
To optimize costs:
- Right-size memory: Test different memory configurations to find the optimal balance of cost and performance
- Optimize code: Minimize external dependencies and cold start time
- Cache formatted results: For frequently formatted code, consider implementing caching
- Implement quotas: Add rate limiting to prevent abuse
Example code size limits:
// Add to your handlers
const MAX_CODE_SIZE = 500 * 1024; // 500KB
if (code.length > MAX_CODE_SIZE) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Code size exceeds maximum allowed size',
limit: `\${MAX_CODE_SIZE/1024}KB`,
actual: `\${code.length/1024}KB`
})
};
}
Security Considerations
When building a code formatting API, consider these security measures:
- Input validation: Strictly validate all inputs to prevent injection attacks
- Rate limiting: Protect against DoS attacks with throttling
- Authentication: Require API keys or more advanced authentication
- Least privilege: Use minimal IAM permissions for Lambda functions
- Code scanning: For APIs that execute code, implement strict sandboxing
- Regular updates: Keep dependencies updated to patch security vulnerabilities
Add CORS headers to limit access to specific domains:
// More restrictive CORS
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': 'https://your-app-domain.com',
'Access-Control-Allow-Headers': 'Content-Type,X-Api-Key',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
},
body: JSON.stringify(response)
};
Conclusion
You've now built a scalable, serverless code formatting API using AWS Lambda and API Gateway. This approach provides a flexible, cost-effective solution that can be used across multiple projects and teams.
This architecture can be extended to support additional formatting tools, languages, and features:
- Add more language formatters (Go, Rust, PHP, etc.)
- Implement custom formatting rules and configuration storage
- Create a web UI for testing the API
- Add user management and organization-specific formatting rules
- Integrate with version control systems for automatic formatting on commit
By leveraging serverless technology, you've created a maintainable, scalable service that separates code formatting concerns from your applications, providing a unified formatting experience across your entire organization.