# Simple, Serverless CRUD with the CDK

https://ryanklein.dev/blog/simple-serverless-cdk
Published 2021-01-04 · Updated 2026-06-21

A practical guide to using AWS CDK for creating serverless applications, with examples of implementing CRUD operations and leveraging higher-level constructs.

**TLDR:** Hands-on tutorial for building serverless CRUD applications with AWS CDK. Learn to set up infrastructure as code, create Lambda functions, and implement authenticated API endpoints with practical examples.

This post provides some basic information on the AWS CDK, along with code examples (updated for CDK v2, the `aws-cdk-lib` package) to get you started.

## How do I set up the AWS CDK?

The first step is to get the [CDK installed](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). This requires:

-   Installing Node.js (the CDK CLI runs on Node even when your app is written in Python)
-   Installing Python 3.9 or later
-   Setting up an AWS account
-   Installing the CDK Toolkit (`npm install -g aws-cdk`)
-   Reviewing [working with the CDK in Python](https://docs.aws.amazon.com/cdk/v2/guide/work-with-cdk-python.html)

## How do I use the CDK CLI?

The two main commands for the CDK cli are `cdk synth` and `cdk deploy`. `synth` will output a template. `deploy` will generate the template and deploy it.

## What does the base CDK template look like?

The AWS docs do a good job of explaining [how to get off the ground](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). The focus here is a backend for simple CRUD operations. Here is a minimal CDK v2 stack (in Python) that stands up a DynamoDB table, a Lambda function, and a REST API in front of it:

```python
from pathlib import Path

from aws_cdk import App, Stack
from aws_cdk import aws_dynamodb as dynamodb
from aws_cdk import aws_lambda as lambda_
from aws_cdk import aws_apigateway as apigw
from constructs import Construct


class CrudStack(Stack):
    def __init__(self, scope: Construct, id: str, **kwargs):
        super().__init__(scope, id, **kwargs)

        table = dynamodb.Table(
            self, "Table",
            partition_key=dynamodb.Attribute(
                name="pk", type=dynamodb.AttributeType.STRING),
            sort_key=dynamodb.Attribute(
                name="sk", type=dynamodb.AttributeType.STRING),
            billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
        )

        handler = lambda_.Function(
            self, "CrudFn",
            runtime=lambda_.Runtime.PYTHON_3_13,
            handler="handler.handler",
            code=lambda_.Code.from_asset(str(Path(__file__).parent / "lambda")),
            environment={"TABLE": table.table_name},
        )

        # One call wires up the IAM policy and role permissions.
        table.grant_read_write_data(handler)

        # A REST API that proxies every route to the function.
        apigw.LambdaRestApi(self, "CrudApi", handler=handler)


app = App()
CrudStack(app, "CrudStack")
app.synth()
```

Note the CDK v2 imports: `App` and `Stack` now come straight from `aws_cdk` (in v1 they lived in `aws_cdk.core`), and the construct base class comes from the separate `constructs` package. Add a Cognito or Lambda authorizer to the API when you need authenticated endpoints.

## Why should I use the CDK?

There are a few reasons why the CDK is awesome!

### You get to work in a language you're familiar with

YAML/JSON can be challenging to work with. There are tools like [cfn-lint](https://github.com/aws-cloudformation/cfn-python-lint), but it can only go so far in linting a YAML file.

Using a language of your choice makes life easier for you and you get extensive type checking, linting, and validation. `synth` and `deploy` ensure that you have a valid template.

### Declarative with use of programming language features

The CDK is highly declarative. Related to higher level constructs, rather than having to explicitly define a policy and attach it to a role, you can do something like:

```python
table.grant_read_write_data(function)
```

The CDK defines all sorts of interfaces to make this possible. All that's required is the argument passed to `grant_read_write_data` adheres to an interface that exposes an IAM principal.

You can also make use of programming language features such as if/else and looping. If you know all of your Lambda functions will use the Python 3.8 runtime, have the same handler function definition, and make use of a table you could create:

```python
def create_python_function(function_name, function_path):
    parent_path = Path(__file__).parent.absolute()
    return lambda_.Function(self, function_name,
            code=lambda_.Code.from_asset(str(Path(parent_path, function_path))),
            runtime=lambda_.Runtime.PYTHON_3_13,
            handler='handler.handler',
            environment={'TABLE': table.table_name})
```

Bye bye boilerplate!

### Higher Level Constructs

The CDK represents CloudFormation resources as constructs. Raw CloudFormation is quite verbose. For the resources that make use of higher level constructs, things are quieted down. For example in the CDK a table may look like:

```python
table = dynamodb.Table(self, 'Table',
    partition_key=dynamodb.Attribute(name='pk', type=dynamodb.AttributeType.STRING),
    sort_key=dynamodb.Attribute(name='sk', type=dynamodb.AttributeType.STRING))

table.add_global_secondary_index(
    partition_key=dynamodb.Attribute(name='sk', type=dynamodb.AttributeType.STRING),
    sort_key=dynamodb.Attribute(name='gsi1sk', type=dynamodb.AttributeType.STRING),
    index_name='gsi1')
```

and a corresponding YAML file may look like:

```yaml
myDynamoDBTable: 
  Type: AWS::DynamoDB::Table
  Properties: 
    AttributeDefinitions: 
      - 
        AttributeName: "Album"
        AttributeType: "S"
      - 
        AttributeName: "Artist"
        AttributeType: "S"
      - 
        AttributeName: "Sales"
        AttributeType: "N"
      - 
        AttributeName: "NumberOfSongs"
        AttributeType: "N"
    KeySchema: 
      - 
        AttributeName: "Album"
        KeyType: "HASH"
      - 
        AttributeName: "Artist"
        KeyType: "RANGE"
    ProvisionedThroughput: 
      ReadCapacityUnits: "5"
      WriteCapacityUnits: "5"
    TableName: "myTableName"
    GlobalSecondaryIndexes: 
      - 
        IndexName: "myGSI"
        KeySchema: 
          - 
            AttributeName: "Sales"
            KeyType: "HASH"
          - 
            AttributeName: "Artist"
            KeyType: "RANGE"
        Projection: 
          NonKeyAttributes: 
            - "Album"
            - "NumberOfSongs"
          ProjectionType: "INCLUDE"
        ProvisionedThroughput: 
          ReadCapacityUnits: "5"
          WriteCapacityUnits: "5"
```

## Where to go next

This same pattern, infrastructure as code deploying Lambda functions behind an API, runs the [Cybersource MLE/JWT Lab](/blog/cybersource-jwt-authentication-with-mle) on this site: its backend is a couple of Lambdas provisioned with the CDK. Because the whole stack is code, it also drops cleanly into a [CI/CD](/blog/ci-cd) pipeline.
