FastAPI
FastAPI es un framework para construir APIs en Python.
El generador de FastAPI crea una nueva aplicación FastAPI con configuración de infraestructura en AWS CDK. El backend generado utiliza AWS Lambda para despliegues serverless, expuesto a través de un API Gateway de AWS. Configura AWS Lambda Powertools para observabilidad, incluyendo logging, trazado con AWS X-Ray y métricas en Cloudwatch.
Uso
Generar una API FastAPI
Puedes generar una nueva API FastAPI de dos formas:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)
en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - py#fast-api
- Complete los parámetros requeridos
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:py#fast-api
yarn nx g @aws/nx-plugin:py#fast-api
npx nx g @aws/nx-plugin:py#fast-api
bunx nx g @aws/nx-plugin:py#fast-api
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:py#fast-api --dry-run
yarn nx g @aws/nx-plugin:py#fast-api --dry-run
npx nx g @aws/nx-plugin:py#fast-api --dry-run
bunx nx g @aws/nx-plugin:py#fast-api --dry-run
Opciones
Parámetro | Tipo | Predeterminado | Descripción |
---|---|---|---|
name Requerido | string | - | Name of the API project to generate |
computeType | string | ServerlessApiGatewayRestApi | The type of compute to use to deploy this API. Choose between ServerlessApiGatewayRestApi (default) or ServerlessApiGatewayHttpApi. |
auth | string | IAM | The method used to authenticate with your API. Choose between IAM (default), Cognito or None. |
directory | string | packages | The directory to store the application in. |
Resultado del generador
El generador creará la siguiente estructura de proyecto en el directorio <directory>/<api-name>
:
- project.json Configuración del proyecto y targets de build
- pyproject.toml Configuración de proyecto Python y dependencias
Directory<module_name>
- __init__.py Inicialización del módulo
- init.py Configura la app FastAPI y middleware de powertools
- main.py Implementación de la API
Directoryscripts
- generate_open_api.py Script para generar esquema OpenAPI desde la app FastAPI
El generador también crea constructs CDK para desplegar tu API, ubicados en el directorio packages/common/constructs
.
Implementando tu FastAPI
La implementación principal de la API está en main.py
. Aquí defines las rutas y sus implementaciones. Ejemplo:
from .init import app, tracerfrom pydantic import BaseModel
class Item(BaseModel): name: str
@app.get("/items/{item_id}")def get_item(item_id: int) -> Item: return Item(name=...)
@app.post("/items")def create_item(item: Item): return ...
El generador configura automáticamente:
- Integración con AWS Lambda Powertools para observabilidad
- Middleware de manejo de errores
- Correlación solicitud/respuesta
- Recolección de métricas
- Handler de AWS Lambda usando Mangum
Observabilidad con AWS Lambda Powertools
Logging
Configura logging estructurado usando AWS Lambda Powertools. Accede al logger en tus handlers:
from .init import app, logger
@app.get("/items/{item_id}")def read_item(item_id: int): logger.info("Fetching item", extra={"item_id": item_id}) return {"item_id": item_id}
El logger incluye automáticamente:
- IDs de correlación para tracing
- Ruta y método de la solicitud
- Información del contexto Lambda
- Indicadores de cold start
Tracing
El tracing con AWS X-Ray se configura automáticamente. Puedes añadir subsegmentos personalizados:
from .init import app, tracer
@app.get("/items/{item_id}")@tracer.capture_methoddef read_item(item_id: int): with tracer.provider.in_subsegment("fetch-item-details"): return {"item_id": item_id}
Métricas
Se recopilan métricas en CloudWatch automáticamente. Añade métricas personalizadas:
from .init import app, metricsfrom aws_lambda_powertools.metrics import MetricUnit
@app.get("/items/{item_id}")def read_item(item_id: int): metrics.add_metric(name="ItemViewed", unit=MetricUnit.Count, value=1) return {"item_id": item_id}
Métricas predeterminadas incluyen:
- Conteo de solicitudes
- Conteos de éxito/fallo
- Métricas de cold start
- Métricas por ruta
Manejo de errores
El generador incluye manejo de errores completo:
from fastapi import HTTPException
@app.get("/items/{item_id}")def read_item(item_id: int): if item_id < 0: raise HTTPException(status_code=400, detail="Item ID must be positive") return {"item_id": item_id}
Excepciones no capturadas son manejadas por el middleware para:
- Registrar la excepción completa con stack trace
- Registrar métrica de fallo
- Retornar respuesta 500 segura al cliente
- Preservar el ID de correlación
Se recomienda especificar modelos de respuesta para las operaciones de la API si usas el generador api-connection
. Más detalles aquí.
Streaming
Con FastAPI, puedes transmitir respuestas usando StreamingResponse
.
Cambios en infraestructura
Como API Gateway de AWS no soporta streaming, necesitarás desplegar en una plataforma que lo permita. La opción más simple es usar Function URL de AWS Lambda. Para esto, reemplaza el constructo generado common/constructs/src/app/apis/<name>-api.ts
con uno que use function URL.
Ejemplo de constructo FunctionURL para streaming
import { Duration, Stack, CfnOutput } from 'aws-cdk-lib';import { IGrantable, Grant } from 'aws-cdk-lib/aws-iam';import { Runtime, Code, Tracing, LayerVersion, FunctionUrlAuthType, InvokeMode, Function,} from 'aws-cdk-lib/aws-lambda';import { Construct } from 'constructs';import url from 'url';import { RuntimeConfig } from '../../core/runtime-config.js';
export class MyApi extends Construct { public readonly handler: Function;
constructor(scope: Construct, id: string) { super(scope, id);
this.handler = new Function(this, 'Handler', { runtime: Runtime.PYTHON_3_12, handler: 'run.sh', code: Code.fromAsset( url.fileURLToPath( new URL( '../../../../../../dist/packages/my_api/bundle', import.meta.url, ), ), ), timeout: Duration.seconds(30), tracing: Tracing.ACTIVE, environment: { AWS_CONNECTION_REUSE_ENABLED: '1', }, });
const stack = Stack.of(this); this.handler.addLayers( LayerVersion.fromLayerVersionArn( this, 'LWALayer', `arn:aws:lambda:${stack.region}:753240598075:layer:LambdaAdapterLayerX86:24`, ), ); this.handler.addEnvironment('PORT', '8000'); this.handler.addEnvironment('AWS_LWA_INVOKE_MODE', 'response_stream'); this.handler.addEnvironment('AWS_LAMBDA_EXEC_WRAPPER', '/opt/bootstrap'); const functionUrl = this.handler.addFunctionUrl({ authType: FunctionUrlAuthType.AWS_IAM, invokeMode: InvokeMode.RESPONSE_STREAM, cors: { allowedOrigins: ['*'], allowedHeaders: [ 'authorization', 'content-type', 'x-amz-content-sha256', 'x-amz-date', 'x-amz-security-token', ], }, });
new CfnOutput(this, 'MyApiUrl', { value: functionUrl.url });
RuntimeConfig.ensure(this).config.apis = { ...RuntimeConfig.ensure(this).config.apis!, MyApi: functionUrl.url, }; }
public grantInvokeAccess(grantee: IGrantable) { Grant.addToPrincipal({ grantee, actions: ['lambda:InvokeFunctionUrl'], resourceArns: [this.handler.functionArn], conditions: { StringEquals: { 'lambda:FunctionUrlAuthType': 'AWS_IAM', }, }, }); }}
Para un ejemplo completo, consulta el Tutorial de Dungeon Adventure
Implementación
Una vez actualizada la infraestructura, puedes implementar streaming en FastAPI. La API debe:
- Retornar un
StreamingResponse
- Declarar el tipo de cada chunk de respuesta
- Añadir la extensión OpenAPI
x-streaming: true
si usas API Connection.
Ejemplo para transmitir objetos JSON:
from pydantic import BaseModelfrom fastapi.responses import StreamingResponse
class Chunk(BaseModel): message: str timestamp: datetime
async def stream_chunks(): for i in range(0, 100): yield Chunk(message=f"This is chunk {i}", timestamp=datetime.now())
@app.get("/stream", openapi_extra={'x-streaming': True})def my_stream() -> Chunk: return StreamingResponse(stream_chunks(), media_type="application/json")
Consumo
Para consumir streams, usa el Generador de Conexión API que provee métodos type-safe para iterar chunks.
Desplegando tu FastAPI
El generador crea un constructo CDK en common/constructs
. Úsalo en una aplicación CDK:
import { MyApi } from ':my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), }); }}
Esto configura:
- Función AWS Lambda por operación
- API Gateway HTTP/REST como trigger
- Roles IAM y permisos
- Log group de CloudWatch
- Configuración de tracing X-Ray
- Namespace de métricas en CloudWatch
Si usas autenticación con Cognito
, debes proveer la propiedad identity
:
import { MyApi, UserIdentity } from ':my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { const identity = new UserIdentity(this, 'Identity');
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), identity, }); }}
El constructo UserIdentity
se genera con el generador ts#cloudscape-website-auth
Integraciones Type-Safe
Los constructos CDK de la API REST/HTTP están configurados para proporcionar una interfaz tipada que permite definir integraciones para cada una de tus operaciones.
Integraciones predeterminadas
Puedes usar el método estático defaultIntegrations
para utilizar el patrón predeterminado, que define una función AWS Lambda individual para cada operación:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
Accediendo a las integraciones
Puedes acceder a las funciones AWS Lambda subyacentes a través de la propiedad integrations
del constructo de la API, de manera tipada. Por ejemplo, si tu API define una operación llamada sayHello
y necesitas agregar permisos a esta función, puedes hacerlo de la siguiente manera:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello está tipado según las operaciones definidas en tu APIapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));
Personalizando opciones predeterminadas
Si deseas personalizar las opciones utilizadas al crear la función Lambda para cada integración predeterminada, puedes usar el método withDefaultOptions
. Por ejemplo, si quieres que todas tus funciones Lambda residan en una VPC:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});
Sobrescribiendo integraciones
También puedes sobrescribir integraciones para operaciones específicas usando el método withOverrides
. Cada sobrescritura debe especificar una propiedad integration
que esté tipada al constructo de integración CDK apropiado para la API HTTP o REST. El método withOverrides
también está tipado. Por ejemplo, si deseas sobrescribir una API getDocumentation
para apuntar a documentación alojada en un sitio web externo:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});
Notarás que la integración sobrescrita ya no tiene una propiedad handler
cuando se accede a través de api.integrations.getDocumentation
.
Puedes agregar propiedades adicionales a una integración que también estarán tipadas, permitiendo abstraer otros tipos de integraciones manteniendo la seguridad de tipos. Por ejemplo, si has creado una integración S3 para una API REST y luego deseas referenciar el bucket para una operación particular:
const storageBucket = new Bucket(this, 'Bucket', { ... });
const apiGatewayRole = new Role(this, 'ApiGatewayS3Role', { assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),});
storageBucket.grantRead(apiGatewayRole);
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getFile: { bucket: storageBucket, integration: new AwsIntegration({ service: 's3', integrationHttpMethod: 'GET', path: `${storageBucket.bucketName}/{fileName}`, options: { credentialsRole: apiGatewayRole, requestParameters: { 'integration.request.path.fileName': 'method.request.querystring.fileName', }, integrationResponses: [{ statusCode: '200' }], }, }), options: { requestParameters: { 'method.request.querystring.fileName': true, }, methodResponses: [{ statusCode: '200', }], } }, }) .build(),});
// Posteriormente, quizás en otro archivo, puedes acceder a la propiedad bucket que definimos// de manera tipadaapi.integrations.getFile.bucket.grantRead(...);
Sobrescribiendo autorizadores
También puedes proporcionar options
en tu integración para sobrescribir opciones de método específicas como autorizadores. Por ejemplo, si deseas usar autenticación de Cognito para tu operación getDocumentation
:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // para REST, o HttpUserPoolAuthorizer para HTTP API } }, }) .build(),});
Integraciones explícitas
Si lo prefieres, puedes optar por no usar las integraciones predeterminadas y en su lugar proporcionar una directamente para cada operación. Esto es útil si, por ejemplo, cada operación necesita usar un tipo diferente de integración o deseas recibir un error de tipo al agregar nuevas operaciones:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});
Patrón de enrutador
Si prefieres desplegar una sola función Lambda para manejar todas las solicitudes de la API, puedes modificar libremente el método defaultIntegrations
de tu API para crear una única función en lugar de una por integración:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { const router = new Function(scope, 'RouterHandler', { ... }); return IntegrationBuilder.rest({ ... defaultIntegrationOptions: {}, buildDefaultIntegration: (op) => { return { // Referencia el mismo manejador Lambda de enrutador en cada integración integration: new LambdaIntegration(router), }; }, }); };}
Puedes modificar el código de otras formas si lo prefieres, por ejemplo, podrías definir la función router
como parámetro de defaultIntegrations
en lugar de construirla dentro del método.
Generación de código
Como las operaciones se definen en Python y la infraestructura en TypeScript, usamos generación de código para proveer metadata type-safe al constructo CDK.
El target generate:<ApiName>-metadata
en project.json
genera archivos como packages/common/constructs/src/generated/my-api/metadata.gen.ts
. Este archivo se ignora en control de versiones.
Debes ejecutar un build tras cambios en la API para actualizar los tipos:
pnpm nx run-many --target build --all
yarn nx run-many --target build --all
npx nx run-many --target build --all
bunx nx run-many --target build --all
Usa nx watch
para regenerar tipos automáticamente durante desarrollo:
pnpm nx watch --projects=<FastAPIProject> -- \pnpm nx run <InfraProject>:"generate:<ApiName>-metadata"
yarn nx watch --projects=<FastAPIProject> -- \yarn nx run <InfraProject>:"generate:<ApiName>-metadata"
npx nx watch --projects=<FastAPIProject> -- \npx nx run <InfraProject>:"generate:<ApiName>-metadata"
bunx nx watch --projects=<FastAPIProject> -- \bunx nx run <InfraProject>:"generate:<ApiName>-metadata"
Otorgando acceso (solo IAM)
Si usas autenticación IAM, otorga acceso con grantInvokeAccess
:
api.grantInvokeAccess(myIdentityPool.authenticatedRole);
Desarrollo local
El generador configura un servidor de desarrollo local. Ejecuta:
pnpm nx run my-api:serve
yarn nx run my-api:serve
npx nx run my-api:serve
bunx nx run my-api:serve
Esto inicia un servidor FastAPI con:
- Auto-recarga ante cambios
- Documentación interactiva en
/docs
o/redoc
- Esquema OpenAPI en
/openapi.json
Invocando tu FastAPI
Para invocar la API desde un sitio React, usa el generador api-connection
.