This is a simple AWS DynamoDB CRUD example written in Golang. Just bear in mind, some files need improvement. There are hard-coded pieces, duplications so on. I had to keep this post as short as possible. Feel free to refactor it.


Structure


├── internal
│   ├── domain
│   │   └── error.go
│   ├── pkg
│   │   └── storage
│   │   ├── aws
│   │   │   ├── aws.go
│   │   │   └── user_storage.go
│   │   └── user_storer.go
│   └── user
│   ├── controller.go
│   └── models.go
├── main.go
└── migrations
└── users.json

Perquisites


First of all you need to create the users table with uuid field as partition key.


$ aws --profile localstack --endpoint-url http://localhost:4566 dynamodb create-table --cli-input-json file://migrations/users.json

# migrations/users.json

{
"TableName": "users",
"AttributeDefinitions": [
{
"AttributeName": "uuid",
"AttributeType": "S"
}
],
"KeySchema": [
{
"AttributeName": "uuid",
"KeyType": "HASH"
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
}
}

Files


main.go


Never mind the endpoints because there shouldn't be suffixes like that!


package main

import (
"log"
"net/http"
"time"

"github.com/you/aws/internal/pkg/storage/aws"
"github.com/you/aws/internal/user"
)

func main() {
// Create a session instance.
ses, err := aws.New(aws.Config{
Address: "http://localhost:4566",
Region: "eu-west-1",
Profile: "localstack",
ID: "test",
Secret: "test",
})
if err != nil {
log.Fatalln(err)
}

// Instantiate HTTP app
usr := user.Controller{
Storage: aws.NewUserStorage(ses, time.Second*5),
}

// Instantiate HTTP router
rtr := http.NewServeMux()
rtr.HandleFunc("/api/v1/users/create", usr.Create)
rtr.HandleFunc("/api/v1/users/find", usr.Find)
rtr.HandleFunc("/api/v1/users/delete", usr.Delete)
rtr.HandleFunc("/api/v1/users/update", usr.Update)

// Start HTTP server
log.Fatalln(http.ListenAndServe(":8080", rtr))
}

controller.go


package user

import (
"encoding/json"
"log"
"net/http"

"github.com/you/aws/internal/domain"
"github.com/you/aws/internal/pkg/storage"

"github.com/google/uuid"
)

type Controller struct {
Storage storage.UserStorer
}

// POST /api/v1/users/create
func (c Controller) Create(w http.ResponseWriter, r *http.Request) {
var req User
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

id := uuid.New().String()

err := c.Storage.Insert(r.Context(), storage.User{
UUID: id,
Name: req.Name,
Level: req.Level,
IsBlocked: req.IsBlocked,
CreatedAt: req.CreatedAt,
Roles: req.Roles,
})
if err != nil {
switch err {
case domain.ErrConflict:
w.WriteHeader(http.StatusConflict)
default:
w.WriteHeader(http.StatusInternalServerError)
}
return
}

w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(id))
}

// GET /api/v1/users/find?id={UUID}
func (c Controller) Find(w http.ResponseWriter, r *http.Request) {
res, err := c.Storage.Find(r.Context(), r.URL.Query().Get("id"))
if err != nil {
switch err {
case domain.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
default:
w.WriteHeader(http.StatusInternalServerError)
}
return
}

user := User{
UUID: res.UUID,
Name: res.Name,
Level: res.Level,
IsBlocked: res.IsBlocked,
CreatedAt: res.CreatedAt,
Roles: res.Roles,
}

data, err := json.Marshal(user)
if err != nil {
log.Println(err)

w.WriteHeader(http.StatusInternalServerError)
}

w.Header().Set("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write(data)
}

// DELETE /api/v1/users/delete?id={UUID}
func (c Controller) Delete(w http.ResponseWriter, r *http.Request) {
err := c.Storage.Delete(r.Context(), r.URL.Query().Get("id"))
if err != nil {
switch err {
case domain.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
default:
w.WriteHeader(http.StatusInternalServerError)
}
return
}

w.WriteHeader(http.StatusNoContent)
}

// PATCH /api/v1/users/update?id={UUID}
func (c Controller) Update(w http.ResponseWriter, r *http.Request) {
var req User
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

err := c.Storage.Update(r.Context(), storage.User{
UUID: r.URL.Query().Get("id"),
Name: req.Name,
Level: req.Level,
Roles: req.Roles,
})
if err != nil {
switch err {
case domain.ErrNotFound:
w.WriteHeader(http.StatusConflict)
default:
w.WriteHeader(http.StatusInternalServerError)
}
return
}

w.WriteHeader(http.StatusNoContent)
}

models.go


package user

import "time"

type User struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Level int `json:"level"`
IsBlocked bool `json:"is_blocked"`
CreatedAt time.Time `json:"created_at"`
Roles []string `json:"roles"`
}

error.go


package domain

import "errors"

var (
ErrInternal = errors.New("internal")
ErrNotFound = errors.New("not found")
ErrConflict = errors.New("conflict")
)

user_storer.go


package storage

import (
"context"
"time"
)

type User struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Level int `json:"level"`
IsBlocked bool `json:"is_blocked"`
CreatedAt time.Time `json:"created_at"`
Roles []string `json:"roles"`
}

type UserStorer interface {
Insert(ctx context.Context, user User) error
Find(ctx context.Context, uuid string) (User, error)
Delete(ctx context.Context, uuid string) error
Update(ctx context.Context, user User) error
}

user_storage.go


package aws

import (
"context"
"fmt"
"log"
"time"

"github.com/you/aws/internal/domain"
"github.com/you/aws/internal/pkg/storage"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)

var _ storage.UserStorer = UserStorage{}

type UserStorage struct {
timeout time.Duration
client *dynamodb.DynamoDB
}

func NewUserStorage(session *session.Session, timeout time.Duration) UserStorage {
return UserStorage{
timeout: timeout,
client: dynamodb.New(session),
}
}

func (u UserStorage) Insert(ctx context.Context, user storage.User) error {
ctx, cancel := context.WithTimeout(ctx, u.timeout)
defer cancel()

item, err := dynamodbattribute.MarshalMap(user)
if err != nil {
log.Println(err)
return domain.ErrInternal
}

input := &dynamodb.PutItemInput{
TableName: aws.String("users"),
Item: item,
ExpressionAttributeNames: map[string]*string{
"#uuid": aws.String("uuid"),
},
ConditionExpression: aws.String("attribute_not_exists(#uuid)"),
}

if _, err := u.client.PutItemWithContext(ctx, input); err != nil {
log.Println(err)

if _, ok := err.(*dynamodb.ConditionalCheckFailedException); ok {
return domain.ErrConflict
}

return domain.ErrInternal
}

return nil
}

func (u UserStorage) Find(ctx context.Context, uuid string) (storage.User, error) {
ctx, cancel := context.WithTimeout(ctx, u.timeout)
defer cancel()

input := &dynamodb.GetItemInput{
TableName: aws.String("users"),
Key: map[string]*dynamodb.AttributeValue{
"uuid": {S: aws.String(uuid)},
},
}

res, err := u.client.GetItemWithContext(ctx, input)
if err != nil {
log.Println(err)

return storage.User{}, domain.ErrInternal
}

if res.Item == nil {
return storage.User{}, domain.ErrNotFound
}

var user storage.User
if err := dynamodbattribute.UnmarshalMap(res.Item, &user); err != nil {
log.Println(err)

return storage.User{}, domain.ErrInternal
}

return user, nil
}

func (u UserStorage) Delete(ctx context.Context, uuid string) error {
ctx, cancel := context.WithTimeout(ctx, u.timeout)
defer cancel()

input := &dynamodb.DeleteItemInput{
TableName: aws.String("users"),
Key: map[string]*dynamodb.AttributeValue{
"uuid": {S: aws.String(uuid)},
},
}

if _, err := u.client.DeleteItemWithContext(ctx, input); err != nil {
log.Println(err)

return domain.ErrInternal
}

return nil
}

func (u UserStorage) Update(ctx context.Context, user storage.User) error {
ctx, cancel := context.WithTimeout(ctx, u.timeout)
defer cancel()

roles := make([]*dynamodb.AttributeValue, len(user.Roles))
for i, role := range user.Roles {
roles[i] = &dynamodb.AttributeValue{S: aws.String(role)}
}

input := &dynamodb.UpdateItemInput{
TableName: aws.String("users"),
Key: map[string]*dynamodb.AttributeValue{
"uuid": {S: aws.String(user.UUID)},
},
ExpressionAttributeNames: map[string]*string{
"#name": aws.String("name"),
"#level": aws.String("level"),
"#roles": aws.String("roles"),
},
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
":name": {S: aws.String(user.Name)},
":level": {N: aws.String(fmt.Sprint(user.Level))},
":roles": {L: roles},
},
UpdateExpression: aws.String("set #name = :name, #level = :level, #roles = :roles"),
ReturnValues: aws.String("UPDATED_NEW"),
}

if _, err := u.client.UpdateItemWithContext(ctx, input); err != nil {
log.Println(err)

return domain.ErrInternal
}

return nil
}

aws.go


package aws

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
)

type Config struct {
Address string
Region string
Profile string
ID string
Secret string
}

func New(config Config) (*session.Session, error) {
return session.NewSessionWithOptions(
session.Options{
Config: aws.Config{
Credentials: credentials.NewStaticCredentials(config.ID, config.Secret, ""),
Region: aws.String(config.Region),
Endpoint: aws.String(config.Address),
S3ForcePathStyle: aws.Bool(true),
},
Profile: config.Profile,
},
)
}

Test


# Create

curl --location --request POST 'http://localhost:8080/api/v1/users/create' \
--data-raw '{
"name": "inanzzz",
"level": 3,
"is_blocked": false,
"created_at": "2020-01-31T23:59:00Z",
"roles": ["accounts", "admin"]
}'

# Find

curl --location --request GET 'http://localhost:8080/api/v1/users/find?id=80638f40-d248-49be-90ce-88d5b1b4ecd4'

# Delete

curl --location --request DELETE 'http://localhost:8080/api/v1/users/delete?id=80638f40-d248-49be-90ce-88d5b1b4ecd4'

# Update

curl --location --request PATCH 'http://localhost:8080/api/v1/users/update?id=347ac592-b024-4001-9d1b-925abe10c236' \
--data-raw '{
"name": "inanzzz",
"level": 1,
"roles": ["accounts", "admin"]
}'

References