Serverless функции на TypeScript: Полное руководство по AWS Lambda
Serverless-архитектура произвела революцию в подходе к разработке бэкенда. Вместо управления серверами разработчики могут сосредоточиться исключительно на бизнес-логике. AWS Lambda — один из самых популярных сервисов для запуска serverless функций. В этом руководстве мы разберем создание, настройку и деплой Lambda-функций на TypeScript — от базовых концепций до production-решений.
Почему TypeScript для AWS Lambda?
TypeScript добавляет статическую типизацию в JavaScript, что критически важно для serverless-функций:
- Безопасность типов — предотвращает ошибки на этапе компиляции
- Автодополнение — IDE подсказывает методы AWS SDK
- Читаемость — интерфейсы и типы документируют структуру данных
- Масштабируемость — легче поддерживать код с ростом проекта
Настройка проекта
1. Инициализация и установка зависимостей
Создайте новый проект и установите необходимые пакеты:
mkdir my-lambda-project
cd my-lambda-project
npm init -y
npm install @types/aws-lambda aws-sdk
npm install -D typescript ts-node @types/node
2. Конфигурация TypeScript
Создайте файл tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Создание первой Lambda-функции
Базовая структура обработчика
Создайте файл src/handler.ts:
import { Handler, APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
interface UserData {
userId: string;
name: string;
email: string;
}
export const handler: Handler<APIGatewayProxyEvent, APIGatewayProxyResult> = async (event) => {
try {
// Парсинг тела запроса
const body: UserData = JSON.parse(event.body || '{}');
// Валидация
if (!body.userId || !body.name) {
return {
statusCode: 400,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({ error: 'Missing required fields' })
};
}
// Бизнес-логика
const result = {
id: body.userId,
displayName: body.name.toUpperCase(),
createdAt: new Date().toISOString()
};
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify(result)
};
} catch (error) {
console.error('Error processing request:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
};
}
};
Работа с окружением и секретами
Используйте переменные окружения для конфигурации:
import { Handler } from 'aws-lambda';
interface Config {
DB_TABLE: string;
MAX_RETRIES: number;
}
export const handler: Handler = async (event) => {
const config: Config = {
DB_TABLE: process.env.DB_TABLE || 'default-table',
MAX_RETRIES: parseInt(process.env.MAX_RETRIES || '3', 10)
};
// Используйте AWS Secrets Manager или Parameter Store
// для чувствительных данных
return {
statusCode: 200,
body: JSON.stringify({ table: config.DB_TABLE })
};
};
Продвинутые техники
1. Обработка ошибок и логирование
Создайте централизованный обработчик ошибок:
// src/utils/error-handler.ts
export class AppError extends Error {
constructor(
public statusCode: number,
public message: string,
public details?: unknown
) {
super(message);
this.name = 'AppError';
}
}
export function formatError(error: unknown) {
if (error instanceof AppError) {
return {
statusCode: error.statusCode,
body: JSON.stringify({
error: error.message,
details: error.details
})
};
}
console.error('Unhandled error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
};
}
2. Middleware для повторяющихся задач
// src/middleware/cors.ts
import { Handler } from 'aws-lambda';
type Middleware = (handler: Handler) => Handler;
export const withCORS: Middleware = (handler) => {
return async (event, context, callback) => {
const response = await handler(event, context, callback);
return {
...response,
headers: {
...response.headers,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE',
'Access-Control-Allow-Headers': 'Content-Type,Authorization'
}
};
};
};
3. Работа с DynamoDB
// src/services/database.ts
import { DynamoDB } from 'aws-sdk';
const dynamoDb = new DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || 'Users';
export interface User {
userId: string;
name: string;
email: string;
createdAt: string;
}
export class UserRepository {
async create(user: User): Promise<User> {
const params = {
TableName: TABLE_NAME,
Item: user,
ConditionExpression: 'attribute_not_exists(userId)'
};
await dynamoDb.put(params).promise();
return user;
}
async getById(userId: string): Promise<User | null> {
const params = {
TableName: TABLE_NAME,
Key: { userId }
};
const result = await dynamoDb.get(params).promise();
return (result.Item as User) || null;
}
async update(userId: string, updates: Partial<User>): Promise<User> {
const updateExpression = Object.keys(updates)
.map(key => `#${key} = :${key}`)
.join(', ');
const expressionAttributeNames = Object.keys(updates)
.reduce((acc, key) => ({ ...acc, [`#${key}`]: key }), {});
const expressionAttributeValues = Object.entries(updates)
.reduce((acc, [key, value]) => ({ ...acc, [`:${key}`]: value }), {});