Salta ai contenuti

Smithy TypeScript API

Smithy è un linguaggio di definizione di interfacce indipendente dal protocollo per creare API in modo modellato.

Il generatore di API Smithy TypeScript crea una nuova API utilizzando Smithy per la definizione del servizio e lo Smithy TypeScript Server SDK per l’implementazione. Il generatore fornisce infrastruttura come codice CDK o Terraform per distribuire il servizio su AWS Lambda, esposto tramite un’API REST AWS API Gateway. Offre sviluppo API type-safe con generazione automatica del codice dai modelli Smithy. L’handler generato utilizza AWS Lambda Powertools for TypeScript per l’osservabilità, inclusi logging, tracciamento AWS X-Ray e metriche CloudWatch.

Puoi generare una nuova API Smithy TypeScript in due modi:

  1. Installa il Nx Console VSCode Plugin se non l'hai già fatto
  2. Apri la console Nx in VSCode
  3. Clicca su Generate (UI) nella sezione "Common Nx Commands"
  4. Cerca @aws/nx-plugin - ts#smithy-api
  5. Compila i parametri richiesti
    • Clicca su Generate
    Parametro Tipo Predefinito Descrizione
    name Obbligatorio 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.

    Il generatore crea due progetti correlati nella directory <directory>/<api-name>:

    • Directorymodel/ Progetto del modello Smithy
      • project.json Configurazione del progetto e target di build
      • smithy-build.json Configurazione di build Smithy
      • build.Dockerfile Configurazione Docker per la creazione di artefatti Smithy
      • Directorysrc/
        • main.smithy Definizione principale del servizio
        • Directoryoperations/
          • echo.smithy Definizione di esempio di un’operazione
    • Directorybackend/ Implementazione TypeScript del backend
      • project.json Configurazione del progetto e target di build
      • rolldown.config.ts Configurazione del bundle
      • Directorysrc/
        • handler.ts Handler AWS Lambda
        • local-server.ts Server di sviluppo locale
        • service.ts Implementazione del servizio
        • context.ts Definizione del contesto del servizio
        • Directoryoperations/
          • echo.ts Implementazione di esempio di un’operazione
        • Directorygenerated/ SDK TypeScript generato (creato durante la build)

    Poiché questo generatore crea infrastruttura come codice in base al iacProvider scelto, genererà un progetto in packages/common che include i costrutti CDK o i moduli Terraform rilevanti.

    Il progetto comune di infrastruttura come codice è strutturato come segue:

    • Directorypackages/common/constructs
      • Directorysrc
        • Directoryapp/ Costrutti per infrastruttura specifica di un progetto/generatore
          • Directoryapis/
            • <project-name>.ts Costrutto CDK per distribuire la tua API
        • Directorycore/ Costrutti generici riutilizzati da quelli in app
          • Directoryapi/
            • rest-api.ts Costrutto CDK per distribuire un’API REST
            • utils.ts Utility per i costrutti API
        • index.ts Punto di ingresso che esporta i costrutti da app
      • project.json Target di build e configurazione del progetto

    Le operazioni sono definite in file Smithy all’interno del progetto del modello. La definizione principale del servizio è in 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,
    // Aggiungi qui le tue operazioni
    ]
    errors: [
    ValidationException
    ]
    }

    Le singole operazioni sono definite in file separati nella directory 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
    }

    Le implementazioni delle operazioni si trovano nella directory src/operations/ del progetto backend. Ogni operazione è implementata utilizzando i tipi generati dall’SDK Server TypeScript (generati al momento della build dal tuo modello Smithy).

    import { ServiceContext } from '../context.js';
    import { Echo as EchoOperation } from '../generated/ssdk/index.js';
    export const Echo: EchoOperation<ServiceContext> = async (input) => {
    // La tua business logic qui
    return {
    message: `Echo: ${input.message}` // type-safe basato sul modello Smithy
    };
    };

    Le operazioni devono essere registrate nella definizione del servizio in src/service.ts:

    import { ServiceContext } from './context.js';
    import { YourServiceService } from './generated/ssdk/index.js';
    import { Echo } from './operations/echo.js';
    // Importa altre operazioni qui
    // Registra le operazioni al servizio qui
    export const Service: YourServiceService<ServiceContext> = {
    Echo,
    // Aggiungi altre operazioni qui
    };

    Puoi definire un contesto condiviso per le tue operazioni in context.ts:

    export interface ServiceContext {
    // Tracer, logger e metrics di Powertools sono forniti di default
    tracer: Tracer;
    logger: Logger;
    metrics: Metrics;
    // Aggiungi dipendenze condivise, connessioni al database, ecc.
    dbClient: any;
    userIdentity: string;
    }

    Questo contesto viene passato a tutte le implementazioni delle operazioni e può essere utilizzato per condividere risorse come connessioni al database, configurazioni o utility di logging.

    Il generatore configura il logging strutturato utilizzando AWS Lambda Powertools con iniezione automatica del contesto tramite middleware Middy.

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

    Puoi accedere al logger dalle implementazioni delle operazioni tramite il contesto:

    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('Il tuo messaggio di log');
    // ...
    };

    Il tracciamento AWS X-Ray è configurato automaticamente tramite il middleware captureLambdaHandler.

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

    Puoi aggiungere sottosegmenti personalizzati ai tuoi trace nelle operazioni:

    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) => {
    // Crea un nuovo sottosegmento
    const subsegment = ctx.tracer.getSegment()?.addNewSubsegment('custom-operation');
    try {
    // La tua logica qui
    } catch (error) {
    subsegment?.addError(error as Error);
    throw error;
    } finally {
    subsegment?.close();
    }
    };

    Le metriche CloudWatch vengono raccolte automaticamente per ogni richiesta tramite il middleware logMetrics.

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

    Puoi aggiungere metriche personalizzate nelle tue operazioni:

    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("CustomMetric", MetricUnit.Count, 1);
    // ...
    };

    Smithy fornisce una gestione degli errori integrata. Puoi definire errori personalizzati nel tuo modello Smithy:

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

    E registrarli alla tua operazione/servizio:

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

    Poi lanciali nella tua implementazione TypeScript:

    import { InvalidRequestError } from '../generated/ssdk/index.js';
    export const MyOperation: MyOperationHandler<ServiceContext> = async (input) => {
    if (!input.requiredField) {
    throw new InvalidRequestError({
    message: "Il campo obbligatorio è mancante"
    });
    }
    return { /* risposta di successo */ };
    };

    Il progetto del modello Smithy utilizza Docker per costruire gli artefatti Smithy e generare l’SDK Server TypeScript:

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

    Questo processo:

    1. Compila il modello Smithy e lo valida
    2. Genera la specifica OpenAPI dal modello Smithy
    3. Crea l’SDK Server TypeScript con interfacce type-safe per le operazioni
    4. Produce artefatti di build in dist/<model-project>/build/

    Il progetto backend copia automaticamente l’SDK generato durante la compilazione:

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

    Il generatore configura automaticamente un target bundle che utilizza Rolldown per creare un pacchetto di distribuzione:

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

    La configurazione di Rolldown si trova in rolldown.config.ts, con un’entry per bundle da generare. Rolldown gestisce la creazione di più bundle in parallelo se definiti.

    Il generatore configura un server di sviluppo locale con hot reloading:

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

    Il generatore crea infrastruttura CDK o Terraform in base al iacProvider selezionato.

    Il costrutto CDK per distribuire la tua API si trova nella cartella common/constructs:

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

    Questo configura:

    1. Una funzione AWS Lambda per il servizio Smithy
    2. API Gateway REST API come trigger della funzione
    3. Ruoli e permessi IAM
    4. Log group CloudWatch
    5. Configurazione del tracciamento X-Ray

    I costrutti CDK per le API REST/HTTP sono configurati per fornire un’interfaccia type-safe per definire le integrazioni per ciascuna delle tue operazioni.

    I costrutti CDK forniscono supporto completo per l’integrazione type-safe come descritto di seguito.

    Puoi utilizzare il metodo statico defaultIntegrations per sfruttare il pattern predefinito, che definisce una singola funzione AWS Lambda per ogni operazione:

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

    Puoi accedere alle funzioni AWS Lambda sottostanti tramite la proprietà integrations del costrutto API in modo type-safe. Ad esempio, se la tua API definisce un’operazione chiamata sayHello e devi aggiungere dei permessi a questa funzione, puoi farlo come segue:

    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });
    // sayHello è tipizzato in base alle operazioni definite nella tua API
    api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({
    effect: Effect.ALLOW,
    actions: [...],
    resources: [...],
    }));

    Se desideri personalizzare le opzioni utilizzate durante la creazione delle funzioni Lambda per ogni integrazione predefinita, puoi usare il metodo withDefaultOptions. Ad esempio, per far risiedere tutte le funzioni Lambda in una VPC:

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

    Puoi sovrascrivere le integrazioni per operazioni specifiche usando il metodo withOverrides. Ogni override deve specificare una proprietà integration tipizzata correttamente. Esempio per reindirizzare un’API getDocumentation:

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

    Le integrazioni sovrascritte non avranno più la proprietà handler quando accedute tramite api.integrations.getDocumentation.

    Puoi aggiungere proprietà personalizzate alle integrazioni mantenendo il type-safety. Esempio con integrazione S3:

    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(),
    });
    // Accesso type-safe alla proprietà bucket definita
    api.integrations.getFile.bucket.grantRead(...);

    Puoi sovrascrivere gli authorizer specificando options nelle integrazioni. Esempio con autenticazione Cognito:

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

    Puoi definire manualmente ogni integrazione. Utile per usare tipi diversi per ogni operazione:

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

    Per usare una singola Lambda per tutte le richieste, modifica defaultIntegrations:

    export class MyApi<...> extends ... {
    public static defaultIntegrations = (scope: Construct) => {
    const router = new Function(scope, 'RouterHandler', { ... });
    return IntegrationBuilder.rest({
    buildDefaultIntegration: (op) => ({
    integration: new LambdaIntegration(router),
    }),
    });
    };
    }

    Poiché le operazioni sono definite in Smithy, utilizziamo la generazione di codice per fornire metadati al costrutto CDK per integrazioni type-safe.

    Un target generate:<ApiName>-metadata è aggiunto al project.json dei costrutti comuni per facilitare questa generazione, producendo un file come packages/common/constructs/src/generated/my-api/metadata.gen.ts. Essendo generato al momento della build, questo file è ignorato dal version control.

    Se hai selezionato autenticazione IAM, puoi usare il metodo grantInvokeAccess per concedere accesso alla tua API:

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

    Per invocare la tua API da un sito React, puoi usare il generatore api-connection, che fornisce generazione type-safe del client dal tuo modello Smithy.