Skip to content

Plain Express routes

Eugene Lazutkin edited this page Jul 16, 2026 · 1 revision

Plain Express routes

Nothing in the toolkit requires the framework adapters — every Adapter method is an ordinary async function you can call from your own routes. This page is the minimal proof: an Express app with three hand-written routes that call dynamodb-toolkit directly, no canned adapter, no route pack, no wire grammar. Use this shape when you want full control over URLs, envelopes, and status codes, or when your API is a handful of endpoints that don't fit the standard pack.

The setup

Same construction as Getting started:

import express from 'express';
import {DynamoDBClient} from '@aws-sdk/client-dynamodb';
import {DynamoDBDocumentClient} from '@aws-sdk/lib-dynamodb';
import {Adapter} from 'dynamodb-toolkit';

const client = DynamoDBDocumentClient.from(new DynamoDBClient({region: 'us-east-1'}), {
  marshallOptions: {removeUndefinedValues: true}
});

const planets = new Adapter({
  client,
  table: 'planets',
  keyFields: ['name']
});

const app = express();
app.use(express.json());

Three routes

// 1. List with paging: GET /planets?offset=0&limit=10
app.get('/planets', async (req, res) => {
  const offset = Math.max(Number(req.query.offset) || 0, 0);
  const limit = Math.min(Number(req.query.limit) || 10, 100);
  const page = await planets.getListByParams({}, {offset, limit});
  res.json(page); // {data, offset, limit, total}
});

// 2. Single item: GET /planets/earth
app.get('/planets/:name', async (req, res) => {
  const item = await planets.getByKey({name: req.params.name});
  if (item === undefined) return res.status(404).end();
  res.json(item);
});

// 3. Create: POST /planets
app.post('/planets', async (req, res) => {
  await planets.post(req.body); // rejects if the key already exists
  res.status(201).end();
});

app.listen(3000);

That is the whole integration. The toolkit consumes and produces plain objects — express.json() supplies the body, res.json() ships the result, and the shape of your URLs and responses is entirely yours.

Error handling

Adapter methods throw on failure. Express 5 forwards rejected promises to the error middleware automatically (on Express 4, wrap each handler), so one error middleware covers all three routes:

app.use((err, req, res, next) => {
  if (res.headersSent) return next(err);
  // post() on an existing key surfaces the SDK's conditional-write rejection
  if (err.name === 'ConditionalCheckFailedException') {
    return res.status(409).json({code: 'AlreadyExists', message: 'item already exists'});
  }
  res.status(err.status >= 400 && err.status < 600 ? err.status : 500).json({
    code: err.code || err.name || 'InternalError',
    message: err.message
  });
});

Toolkit errors carry name (matching the class name) and often status; SDK errors keep their SDK names. Map the ones you care about, default the rest.

Growing from here

Every other Adapter method drops in the same way — patch for partial updates, delete, getByKeys for bulk reads, the mass operations (Adapter: CRUD methods, Adapter: Mass methods). The toolkit's other layers stay available à la carte: the expression builders compose raw SDK params, and the REST core parsers/builders are importable individually if you want the standard filter grammar or envelope shape inside your own routes.

When the endpoint count grows and you find yourself re-implementing the standard pack — list envelopes with paging links, -by-names bulk routes, clone/move, the filter grammar — that is what the framework adapters ship in one line; the Express adapter coexists happily with hand-written routes on the same app (mount it on one path prefix, keep custom routes on others).

Clone this wiki locally