tutorial
Build a CRUD App with Node and DynamoDB, Part 2
Last updated 28 September 2026
Part 1 ended with a page that reads products from your own DynamoDB table through a Lambda function. This part adds the two verbs that make it an application: adding a product through the grid's built-in form, and deleting one from the right-click menu. Both land in DynamoDB before the page moves on.
Everything from part 1 carries over. You will change the function's code, widen its permissions and its URL settings, and swap the page.
Source: the part-2 folder
of toclocoinc/lattice-tutorial-crud-dynamodb
(Code,
Download ZIP). This part needs Lattice Grid 1.76.0 or later, which is the version
the page loads.
Step 1: teach the function two more verbs
Open Lambda, then products-read, then the
Code tab. Replace the contents of index.mjs with
lambda/index.mjs from the download and choose Deploy.
The function now looks at the request's method:
GETreturns every product, as before.-
POSTtakes a product as JSON, gives it askuif the page did not, stampsupdatedwith today's date, and writes it with a condition that refuses to overwrite an existingsku. It answers with the product as stored. DELETEtakes?sku=and removes that product.
The condition on the write is the one line worth pausing on. Without it, two people adding a product
with the same sku would silently overwrite each other. With it, the second one gets a
clear "already exists" answer that the page can show.
Step 2: allow the new verbs through the URL
The Function URL's CORS setting from part 1 allows every origin but only the GET
method, so the browser would refuse to send a POST or a DELETE.
- On the function's Configuration tab, choose Function URL, then Edit.
-
Under Configure cross-origin resource sharing (CORS), set Allow
methods to
GET,POSTandDELETE, and Allow headers tocontent-type. The page sends that header with the JSON body of a POST. - Save.
Step 3: give the function the permission it needs, and no more
Part 1 attached AmazonDynamoDBReadOnlyAccess, which is enough to read and
nothing else. The function now writes, so it needs PutItem and
DeleteItem, and the tidy way to grant them is a policy that names this one table.
- On the Configuration tab, choose Permissions, then click the role name under Execution role. IAM opens.
-
Under Permissions policies, tick
AmazonDynamoDBReadOnlyAccessand choose Remove. Confirm. -
Choose Add permissions, then Create inline policy. Switch the
editor to JSON and paste
iam/products-policy.jsonfrom the download. ReplaceREGIONwith your region (eu-west-2for London) andACCOUNT_IDwith the twelve-digit account id shown at the top right of the console. - Choose Next, name it
products-table, and Create policy.
The policy allows scan, get, put, update and delete on the Products table and
nothing on any other table. Update is not used yet; part 3 needs it, and it saves a return trip to
IAM.
Step 4: the page
Open web/index.html from the download and paste your Function URL into the same line
as in part 1. Open the file in a browser.
Three things are new.
Add product opens the grid's built-in form for a new row. Fill in a name, category,
quantity and price, and choose Save. The form validates the fields the way the grid's own editors
do, then calls the create hook you gave it:
rowForm: {
fields: [
{ field: 'name', label: 'Product' },
{ field: 'category', label: 'Category' },
{ field: 'quantity', label: 'In stock' },
{ field: 'price', label: 'Price' },
],
create: (values) => api('POST', '', values),
},
create posts the values to the function and returns what comes back: the product as
DynamoDB stored it, with the sku the server assigned. The grid adds that row, scrolls
to it and selects it. If the function refuses, the form stays open with your values and a retry, and
the status line shows the server's reason.
Right-click a row and the menu has the grid's own items, then a Delete for that product:
contextMenu: (p, defaults) => [
...defaults,
{ separator: true },
{
name: 'Delete ' + p.data.name,
action: async () => {
if (!confirm('Delete ' + p.data.name + '?')) return;
await api('DELETE', '?sku=' + encodeURIComponent(p.key));
grid.rows.apply({ remove: [p.key] });
},
},
],
p describes the cell that was clicked: p.key is the row's key, the
sku, and p.data is your product object. The action deletes on the server
first and only then removes the row from the grid, so a failed delete leaves the row where it was.
The api helper is eight lines that wrap fetch, set the JSON header when
there is a body, and turn a non-2xx answer into an error carrying the function's message. Every call
in the page goes through it.
Step 5: check the table
Add a product, then open DynamoDB, Tables, Products,
Explore table items. The new product is there with the sku the
function chose. Delete it from the page and refresh the console; it is gone.
What you have
A page that reads, adds and deletes against your own table, with every write happening on the server before the page reflects it. The grid supplied the form and the menu; you supplied two verbs and a policy.
Next
Part 3: edit in place. Cells become editable and each committed change is patched to DynamoDB while the grid shows it, with the old value put back if the server says no. The page also goes full screen on a button, and the form moves from a drawer into a panel of your own.
Cleaning up
As in part 1: delete the function, the table and the role. The inline policy goes with the role.
Read the full source, including lambda/index.mjs and web/index.html, on
the public repository, or grab it as a
ZIP download and skip the cloning.