Deploy

Description

One of the great things about this library is that it can be deployed to serverless environments. It's super small footprint makes it lightning fast to load, even on cold starts.
The basic use case is simple: we simply need to forward request from some API to our read/write models.
Let's give it a shot using Express:
app.js
1
const {
2
readModel,
3
writeModel,
4
} = require('./src')
5
6
const express = require('express')
7
const app = express()
8
9
app.use(express.json())
10
11
app.get('/:id', (req, res, next) => {
12
readModel.getById({
13
id: req.params.id,
14
})
15
.then(obj => {
16
if (!obj) throw new Error('Not Found')
17
res.json(obj)
18
})
19
.catch(next)
20
})
21
22
app.post('/', (req, res, next) => {
23
writeModel.addTodo(req.body.id, {
24
title: req.body.title,
25
})
26
.then(res.json.bind(res))
27
.catch(next)
28
})
29
30
app.use(function(error, req, res, next) {
31
console.log(error)
32
res.status(500).json({ message: error.message });
33
});
34
35
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
Copied!
Or we can use GraphQL, which works really nicely with our setup. Mutations map directly to the write model and Queries to the read model.
app.js
1
const { GraphQLServer } = require('graphql-yoga')
2
const {
3
readModel,
4
writeModel,
5
} = require('./src')
6
7
const typeDefs = `
8
type Todo {
9
title: String!
10
completed: Boolean
11
}
12
13
type TodoList {
14
id: ID!
15
todos: [Todo]
16
}
17
18
type Query {
19
getById(id: ID!): TodoList
20
}
21
22
type Mutation {
23
addTodo(id: ID!, title: String!): Boolean
24
}
25
`
26
27
const resolvers = {
28
Mutation: {
29
addTodo: (_, { id, title }) => {
30
return writeModel.addTodo(id, { title })
31
},
32
},
33
Query: {
34
getById: (_, { id }) => {
35
return readModel.getById({ id })
36
},
37
},
38
}
39
40
const server = new GraphQLServer({ typeDefs, resolvers })
41
server.start(() => console.log('Server is running on localhost:4000'))
Copied!
We're really not doing very much in either of these examples, we simply forward requests from the API to our read or write model. In the case of GraphQL, it's a bit more explicit.

Serverless Framwork

Now let's look at deploying using the awesome serverless framework.
Following this quickstart, let’s set up our serverless app:
1
$ npm install -g serverless
2
$ serverless create --template aws-nodejs --path my-service
3
$ cd my-service
Copied!
(you’ll also need AWS credentials in your local environment)
Now replace the contents of serverless.yml with this:
serverless.yml
1
service: my-serverless-cqrs-service
2
3
## let's define some constants to use later in this file
4
custom:
5
writeModelTableName: commitsByEntityIdAndVersion
6
writeModelIndexName: indexByEntityNameAndCommitId
7
readModelDomainName: readmodel
8
9
provider:
10
name: aws
11
runtime: nodejs8.10
12
## make these values available in process.env
13
environment:
14
WRITE_MODEL_TABLENAME: ${self:custom.writeModelTableName}
15
WRITE_MODEL_INDEXNAME: ${self:custom.writeModelIndexName}
16
ELASTIC_READ_MODEL_ENDPOINT:
17
Fn::GetAtt:
18
- ReadModelProjections
19
- DomainEndpoint
20
21
## allow our lambda functions to access to following resources
22
iamRoleStatements:
23
- Effect: Allow
24
Action:
25
- "dynamodb:*"
26
Resource: "arn:aws:dynamodb:*:*:table/${self:custom.writeModelTableName}*"
27
- Effect: "Allow"
28
Action:
29
- "es:*"
30
Resource:
31
- "arn:aws:es:*:*:domain/${self:custom.readModelDomainName}/*"
32
- Effect: "Allow"
33
Action:
34
- "es:ESHttpGet"
35
Resource:
36
- "*"
37
38
## Create an API Gateway and connect it to our handler function
39
functions:
40
router:
41
handler: app.handler
42
events:
43
- http: ANY /
44
- http: ANY {proxy+}
45
46
resources:
47
Resources:
48
49
## create a DynamoDB table for storing events
50
EventStoreTable:
51
Type: 'AWS::DynamoDB::Table'
52
Properties:
53
TableName: ${self:custom.writeModelTableName}
54
AttributeDefinitions:
55
- AttributeName: entityId
56
AttributeType: S
57
- AttributeName: version
58
AttributeType: N
59
- AttributeName: entityName
60
AttributeType: S
61
- AttributeName: commitId
62
AttributeType: S
63
KeySchema:
64
- AttributeName: entityId
65
KeyType: HASH
66
- AttributeName: version
67
KeyType: RANGE
68
ProvisionedThroughput:
69
ReadCapacityUnits: 5
70
WriteCapacityUnits: 5
71
GlobalSecondaryIndexes:
72
- IndexName: ${self:custom.writeModelIndexName}
73
KeySchema:
74
- AttributeName: entityName
75
KeyType: HASH
76
- AttributeName: commitId
77
KeyType: RANGE
78
Projection:
79
ProjectionType: ALL
80
ProvisionedThroughput:
81
ReadCapacityUnits: 5
82
WriteCapacityUnits: 5
83
84
## create an ElasticSearch instance for storing read model projections
85
ReadModelProjections:
86
Type: 'AWS::Elasticsearch::Domain'
87
Properties:
88
DomainName: ${self:custom.readModelDomainName}
89
ElasticsearchVersion: 6.2
90
EBSOptions:
91
EBSEnabled: true
92
VolumeSize: 10
93
VolumeType: gp2
94
ElasticsearchClusterConfig:
95
InstanceType: t2.small.elasticsearch
Copied!
The AWS Free Tier covers most of these services, but running an ElasticSearch instance on AWS can be expensive. Make sure to run serverless remove once you're done.
Now let's modify the example above to have it work on AWS Lambda
app.js
1
const express = require('express')
2
const serverless = require('serverless-http')
3
// ^ add this
4
5
/// ...same contents as above
6
7
module.exports.handler = serverless(app)
8
// ^ add this
9
10
// app.listen(port, () => console.log(`Example app listening on port ${port}!`))
11
// ^ remove this
Copied!

Examples

The domain in these examples also contain the commands completeTodo and removeTodo.
Last modified 1yr ago