Pular para o conteúdo

API TypeScript do Smithy

Smithy é uma linguagem de definição de interface independente de protocolo para criação de APIs de forma orientada a modelos.

O gerador de API Smithy TypeScript cria uma nova API usando Smithy para definição de serviços e o Smithy TypeScript Server SDK para implementação. O gerador fornece infraestrutura como código via CDK ou Terraform para implantar seu serviço na AWS Lambda, exposto através de uma API REST do AWS API Gateway. Oferece desenvolvimento de API com tipagem segura e geração automática de código a partir de modelos Smithy. O handler gerado utiliza AWS Lambda Powertools for TypeScript para observabilidade, incluindo logging, rastreamento com AWS X-Ray e métricas no CloudWatch.

Você pode gerar uma nova API Smithy TypeScript de duas formas:

  1. Instale o Nx Console VSCode Plugin se ainda não o fez
  2. Abra o console Nx no VSCode
  3. Clique em Generate (UI) na seção "Common Nx Commands"
  4. Procure por @aws/nx-plugin - ts#smithy-api
  5. Preencha os parâmetros obrigatórios
    • Clique em Generate
    Parâmetro Tipo Padrão Descrição
    name Obrigatório string - The name of the API (required). Used to generate class names and file paths.
    namespace string - The namespace for the Smithy API. Defaults to your monorepo scope
    computeType string ServerlessApiGatewayRestApi The type of compute to use to deploy this API.
    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.
    iacProvider string Inherit The preferred IaC provider. By default this is inherited from your initial selection.

    O gerador cria dois projetos relacionados no diretório <directory>/<api-name>:

    • Directorymodel/ Projeto de modelo Smithy
      • project.json Configuração do projeto e targets de build
      • smithy-build.json Configuração de build Smithy
      • build.Dockerfile Configuração Docker para construir artefatos Smithy
      • Directorysrc/
        • main.smithy Definição principal do serviço
        • Directoryoperations/
          • echo.smithy Exemplo de definição de operação
    • Directorybackend/ Implementação TypeScript do backend
      • project.json Configuração do projeto e targets de build
      • rolldown.config.ts Configuração de bundle
      • Directorysrc/
        • handler.ts Handler AWS Lambda
        • local-server.ts Servidor local de desenvolvimento
        • service.ts Implementação do serviço
        • context.ts Definição de contexto do serviço
        • Directoryoperations/
          • echo.ts Exemplo de implementação de operação
        • Directorygenerated/ SDK TypeScript gerado (criado durante o build)

    Como este gerador cria infraestrutura como código baseada no iacProvider escolhido, ele criará um projeto em packages/common que inclui os constructs CDK ou módulos Terraform relevantes.

    O projeto comum de infraestrutura como código é estruturado da seguinte forma:

    • Directorypackages/common/constructs
      • Directorysrc
        • Directoryapp/ Constructs para infraestrutura específica de um projeto/gerador
          • Directoryapis/
            • <project-name>.ts Construct CDK para implantar sua API
        • Directorycore/ Constructs genéricos reutilizados por constructs em app
          • Directoryapi/
            • rest-api.ts Construct CDK para implantar uma API REST
            • utils.ts Utilitários para os constructs de API
        • index.ts Ponto de entrada exportando constructs de app
      • project.json Targets de build e configuração do projeto

    Operações são definidas em arquivos Smithy dentro do projeto de modelo. A definição principal do serviço está em main.smithy:

    $version: "2.0"
    namespace your.namespace
    use aws.protocols#restJson1
    use smithy.framework#ValidationException
    @title("YourService")
    @restJson1
    service YourService {
    version: "1.0.0"
    operations: [
    Echo,
    // Adicione suas operações aqui
    ]
    errors: [
    ValidationException
    ]
    }

    Operações individuais são definidas em arquivos separados no diretório operations/:

    $version: "2.0"
    namespace your.namespace
    @http(method: "POST", uri: "/echo")
    operation Echo {
    input: EchoInput
    output: EchoOutput
    }
    structure EchoInput {
    @required
    message: String
    foo: Integer
    bar: String
    }
    structure EchoOutput {
    @required
    message: String
    }

    As implementações das operações estão localizadas no diretório src/operations/ do projeto backend. Cada operação é implementada usando os tipos gerados do TypeScript Server SDK (gerados em tempo de build a partir do seu modelo Smithy).

    import { ServiceContext } from '../context.js';
    import { Echo as EchoOperation } from '../generated/ssdk/index.js';
    export const Echo: EchoOperation<ServiceContext> = async (input) => {
    // Sua lógica de negócios aqui
    return {
    message: `Echo: ${input.message}` // tipagem segura baseada no seu modelo Smithy
    };
    };

    As operações devem ser registradas na definição do serviço em src/service.ts:

    import { ServiceContext } from './context.js';
    import { YourServiceService } from './generated/ssdk/index.js';
    import { Echo } from './operations/echo.js';
    // Importe outras operações aqui
    // Registre as operações no serviço aqui
    export const Service: YourServiceService<ServiceContext> = {
    Echo,
    // Adicione outras operações aqui
    };

    Você pode definir contexto compartilhado para suas operações em context.ts:

    export interface ServiceContext {
    // Tracer, logger e metrics do Powertools são fornecidos por padrão
    tracer: Tracer;
    logger: Logger;
    metrics: Metrics;
    // Adicione dependências compartilhadas, conexões de banco de dados, etc.
    dbClient: any;
    userIdentity: string;
    }

    Este contexto é passado para todas as implementações de operações e pode ser usado para compartilhar recursos como conexões de banco de dados, configuração ou utilitários de logging.

    O gerador configura logging estruturado usando AWS Lambda Powertools com injeção automática de contexto via middleware Middy.

    handler.ts
    export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>()
    .use(captureLambdaHandler(tracer))
    .use(injectLambdaContext(logger))
    .use(logMetrics(metrics))
    .handler(lambdaHandler);

    Você pode referenciar o logger das suas implementações de operações via contexto:

    operations/echo.ts
    import { ServiceContext } from '../context.js';
    import { Echo as EchoOperation } from '../generated/ssdk/index.js';
    export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => {
    ctx.logger.info('Sua mensagem de log');
    // ...
    };

    O rastreamento com AWS X-Ray é configurado automaticamente via middleware captureLambdaHandler.

    handler.ts
    export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>()
    .use(captureLambdaHandler(tracer))
    .use(injectLambdaContext(logger))
    .use(logMetrics(metrics))
    .handler(lambdaHandler);

    Você pode adicionar subsegmentos personalizados aos seus traces nas operações:

    operations/echo.ts
    import { ServiceContext } from '../context.js';
    import { Echo as EchoOperation } from '../generated/ssdk/index.js';
    export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => {
    // Cria um novo subsegmento
    const subsegment = ctx.tracer.getSegment()?.addNewSubsegment('operacao-personalizada');
    try {
    // Sua lógica aqui
    } catch (error) {
    subsegment?.addError(error as Error);
    throw error;
    } finally {
    subsegment?.close();
    }
    };

    Métricas do CloudWatch são coletadas automaticamente para cada requisição via middleware logMetrics.

    handler.ts
    export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>()
    .use(captureLambdaHandler(tracer))
    .use(injectLambdaContext(logger))
    .use(logMetrics(metrics))
    .handler(lambdaHandler);

    Você pode adicionar métricas personalizadas nas suas operações:

    operations/echo.ts
    import { MetricUnit } from '@aws-lambda-powertools/metrics';
    import { ServiceContext } from '../context.js';
    import { Echo as EchoOperation } from '../generated/ssdk/index.js';
    export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => {
    ctx.metrics.addMetric("MetricaPersonalizada", MetricUnit.Count, 1);
    // ...
    };

    Smithy fornece tratamento de erros embutido. Você pode definir erros personalizados no seu modelo Smithy:

    @error("client")
    @httpError(400)
    structure InvalidRequestError {
    @required
    message: String
    }

    E registrá-los na sua operação/serviço:

    operation MyOperation {
    ...
    errors: [InvalidRequestError]
    }

    Então lançá-los na implementação TypeScript:

    import { InvalidRequestError } from '../generated/ssdk/index.js';
    export const MyOperation: MyOperationHandler<ServiceContext> = async (input) => {
    if (!input.requiredField) {
    throw new InvalidRequestError({
    message: "Campo obrigatório faltando"
    });
    }
    return { /* resposta de sucesso */ };
    };

    O projeto de modelo Smithy usa Docker para construir os artefatos Smithy e gerar o TypeScript Server SDK:

    Terminal window
    pnpm nx run <model-project>:build

    Este processo:

    1. Compila o modelo Smithy e o valida
    2. Gera especificação OpenAPI a partir do modelo Smithy
    3. Cria TypeScript Server SDK com interfaces de operação tipadas
    4. Gera artefatos de build em dist/<model-project>/build/

    O projeto backend copia automaticamente o SDK gerado durante a compilação:

    Terminal window
    pnpm nx run <backend-project>:copy-ssdk

    O gerador configura automaticamente um destino bundle que utiliza o Rolldown para criar um pacote de implantação:

    Terminal window
    pnpm nx run <project-name>:bundle

    A configuração do Rolldown pode ser encontrada em rolldown.config.ts, com uma entrada para cada pacote a ser gerado. O Rolldown gerencia a criação de múltiplos pacotes em paralelo, se definidos.

    O gerador configura um servidor de desenvolvimento local com hot reloading:

    Terminal window
    pnpm nx run <backend-project>:serve

    O gerador cria infraestrutura CDK ou Terraform baseada no iacProvider selecionado.

    O construct CDK para implantar sua API está na pasta common/constructs:

    import { MyApi } from ':my-scope/common-constructs';
    export class ExampleStack extends Stack {
    constructor(scope: Construct, id: string) {
    // Adicione a API à sua stack
    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });
    }
    }

    Isso configura:

    1. Uma função AWS Lambda para o serviço Smithy
    2. API Gateway REST API como trigger da função
    3. Roles e permissões IAM
    4. Grupo de logs CloudWatch
    5. Configuração de rastreamento X-Ray

    Os construtos CDK da API REST/HTTP são configurados para fornecer uma interface type-safe para definir integrações para cada uma de suas operações.

    Os construtos CDK fornecem suporte completo a integrações type-safe conforme descrito abaixo.

    Você pode usar o método estático defaultIntegrations para utilizar o padrão padrão, que define uma função AWS Lambda individual para cada operação:

    new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });

    Você pode acessar as funções AWS Lambda subjacentes através da propriedade integrations do construto da API, de forma type-safe. Por exemplo, se sua API define uma operação chamada sayHello e você precisa adicionar permissões a esta função, você pode fazer isso da seguinte forma:

    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });
    // sayHello é tipado conforme as operações definidas em sua API
    api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({
    effect: Effect.ALLOW,
    actions: [...],
    resources: [...],
    }));

    Se você deseja personalizar as opções usadas ao criar a função Lambda para cada integração padrão, pode usar o método withDefaultOptions. Por exemplo, se deseja que todas suas funções Lambda residam em uma VPC:

    const vpc = new Vpc(this, 'Vpc', ...);
    new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this)
    .withDefaultOptions({
    vpc,
    })
    .build(),
    });

    Você também pode sobrescrever integrações para operações específicas usando o método withOverrides. Cada sobrescrita deve especificar uma propriedade integration que é tipada ao construto de integração CDK apropriado para a API HTTP ou REST. O método withOverrides também é type-safe. Por exemplo, se você deseja sobrescrever uma API getDocumentation para apontar para documentação hospedada em um site externo:

    new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this)
    .withOverrides({
    getDocumentation: {
    integration: new HttpIntegration('https://example.com/documentation'),
    },
    })
    .build(),
    });

    Você notará que a integração sobrescrita não terá mais uma propriedade handler quando acessada via api.integrations.getDocumentation.

    Você pode adicionar propriedades adicionais a uma integração que também serão tipadas adequadamente, permitindo que outros tipos de integração sejam abstraídos mas permaneçam type-safe. Por exemplo, se você criou uma integração S3 para uma API REST e depois deseja referenciar o bucket para uma operação específica:

    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, talvez em outro arquivo, você pode acessar a propriedade bucket que definimos
    // de forma type-safe
    api.integrations.getFile.bucket.grantRead(...);

    Você também pode fornecer options em sua integração para sobrescrever opções específicas de método como autorizadores. Por exemplo, se desejar usar autenticação Cognito para sua operação getDocumentation:

    new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this)
    .withOverrides({
    getDocumentation: {
    integration: new HttpIntegration('https://example.com/documentation'),
    options: {
    authorizer: new CognitoUserPoolsAuthorizer(...) // para REST, ou HttpUserPoolAuthorizer para HTTP API
    }
    },
    })
    .build(),
    });

    Se preferir, você pode optar por não usar as integrações padrão e fornecer diretamente uma para cada operação. Isso é útil se, por exemplo, cada operação precisar usar um tipo diferente de integração ou se você quiser receber um erro de tipo ao adicionar novas operações:

    new MyApi(this, 'MyApi', {
    integrations: {
    sayHello: {
    integration: new LambdaIntegration(...),
    },
    getDocumentation: {
    integration: new HttpIntegration(...),
    },
    },
    });

    Se você preferir implantar uma única função Lambda para atender todas as requisições da API, pode livremente editar o método defaultIntegrations de sua API para criar uma única função em vez de uma por integração:

    packages/common/constructs/src/app/apis/my-api.ts
    export class MyApi<...> extends ... {
    public static defaultIntegrations = (scope: Construct) => {
    const router = new Function(scope, 'RouterHandler', { ... });
    return IntegrationBuilder.rest({
    ...
    defaultIntegrationOptions: {},
    buildDefaultIntegration: (op) => {
    return {
    // Referencia o mesmo router lambda handler em todas as integrações
    integration: new LambdaIntegration(router),
    };
    },
    });
    };
    }

    Você pode modificar o código de outras formas se preferir, por exemplo pode definir a função router como parâmetro para defaultIntegrations em vez de construí-la dentro do método.

    Como as operações são definidas em Smithy, usamos geração de código para fornecer metadados ao construct CDK para integrações tipadas.

    Um target generate:<ApiName>-metadata é adicionado ao project.json dos constructs comuns para facilitar esta geração de código, que emite um arquivo como packages/common/constructs/src/generated/my-api/metadata.gen.ts. Como isso é gerado em tempo de build, é ignorado no controle de versão.

    Se você selecionou autenticação IAM, pode usar o método grantInvokeAccess para conceder acesso à sua API:

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

    Para invocar sua API de um website React, você pode usar o gerador api-connection, que fornece geração de cliente tipado a partir do seu modelo Smithy.