npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@verisure-italy/express-router-middleware

v1.8.8

Published

Express middleware for Verisure Italy AAA

Readme

Router Middleware

A modern, type-safe router middleware for Express that integrates DynamoDB, Zod validation, authentication, and authorization.

Features

  • Type-safe - Full TypeScript typing with automatic inference
  • Zod Validation - Reusable and type-safe schemas
  • DynamoDB Integration - Automatic CRUD via dynamo-kit
  • ACL with UserRole - Typed role-based access control
  • REST Controllers - Ready-to-use CRUD operations with generics
  • Param Resolver - Automatic entity loading from URL
  • Entity Enrichment - Type-safe data enrichment
  • Projection Fields - Field selection with autocomplete
  • Generic Middlewares - All middlewares support type inference

Installation

pnpm add @verisure-italy/router-middleware

Basic Usage with Type Safety

1. Define Your Data Models

import { AccessToken, User } from '@verisure-italy/aaa-types'
import { createRouter, restControllers, RouteConfig } from '@verisure-italy/router-middleware'
import { z } from 'zod'

// Define your data model types
interface DataModels {
  accessToken: AccessToken
  user: User
}

2. Configure Router with Type Safety

const router = createRouter<DataModels>(
  [
    // ✅ GET /access-tokens/:accessToken - Read with type-safe projection fields
    {
      method: 'get',
      path: '/access-tokens/:accessToken',
      handler: restControllers.read<AccessToken>,
      options: {
        dataModel: 'accessToken',
        // ✅ TypeScript knows these are AccessToken fields!
        projectionFields: ['id', 'token', 'user', 'expires'],
        secure: true,
        acl: {
          allow: {
            // ✅ UserRole typed with autocomplete!
            roles: ['ROLE_AAA_ADMIN', 'ROLE_AAA_READER']
          }
        }
      }
    } satisfies RouteConfig<string, AccessToken>,

    // ✅ POST /access-tokens - Create with validation
    {
      method: 'post',
      path: '/access-tokens',
      handler: restControllers.create<AccessToken>,
      options: {
        dataModel: 'accessToken',
        secure: true,
        validate: {
          body: z.object({
            token: z.string().min(1),
            user: z.string(),
            client: z.object({
              id: z.string()
            }),
            expires: z.number().positive(),
            scope: z.string().optional()
          })
        },
        acl: {
          allow: {
            roles: ['ROLE_AAA_ADMIN']
          }
        }
      }
    } satisfies RouteConfig<string, AccessToken>,

    // ✅ GET /access-tokens - List
    {
      method: 'get',
      path: '/access-tokens',
      handler: restControllers.list<AccessToken>,
      options: {
        dataModel: 'accessToken',
        secure: true,
        acl: {
          allow: {
            roles: ['ROLE_AAA_ADMIN', 'ROLE_AAA_READER']
          }
        }
      }
    } satisfies RouteConfig<string, AccessToken>,

    // ✅ PUT /access-tokens/:accessToken - Update
    {
      method: 'put',
      path: '/access-tokens/:accessToken',
      handler: restControllers.update<AccessToken>,
      options: {
        dataModel: 'accessToken',
        secure: true,
        validate: {
          body: z.object({
            expires: z.number().positive().optional(),
            scope: z.string().optional()
          })
        },
        acl: {
          allow: {
            roles: ['ROLE_AAA_ADMIN']
          }
        }
      }
    } satisfies RouteConfig<string, AccessToken>,

    // ✅ DELETE /access-tokens/:accessToken
    {
      method: 'delete',
      path: '/access-tokens/:accessToken',
      handler: restControllers.deleteEntity<AccessToken>,
      options: {
        dataModel: 'accessToken',
        secure: true,
        acl: {
          allow: {
            roles: ['ROLE_AAA_ADMIN']
          }
        }
      }
    } satisfies RouteConfig<string, AccessToken>,
  ],
  {
    // ✅ Data model configuration with type safety
    dataModels: {
      accessToken: {
        tableName: 'access_token',
        idField: 'id'
      },
      user: {
        tableName: 'user',
        idField: 'id'
      }
    },
    dynamoConfig: {
      region: 'eu-west-1'
    }
  }
)

export default router

3. Custom Handlers with Type Safety

import { Request, Response, NextFunction } from 'express'
import { AccessToken } from '@verisure-italy/aaa-types'

// ✅ Custom handler with type safety
const customTokenHandler = async (req: Request, res: Response, next: NextFunction) => {
  try {
    // ✅ req.accessToken is typed as AccessToken!
    const token: AccessToken = req.accessToken
    
    // ✅ TypeScript knows all available fields
    const isExpired = token.expires < Math.floor(Date.now() / 1000)
    
    res.json({
      token: token.token,
      isExpired,
      user: token.user
    })
  } catch (error) {
    next(error)
  }
}

// Use the custom handler
{
  method: 'get',
  path: '/access-tokens/:accessToken/status',
  handler: customTokenHandler,
  options: {
    dataModel: 'accessToken',
    secure: true
  }
} satisfies RouteConfig<string, AccessToken>

Advanced Features

Lifecycle Hooks

Execute custom logic before and after entity operations (create, update, delete). Hooks are perfect for:

  • Data modification: Add timestamps, normalize data, generate computed fields
  • Validation: Enforce business rules beyond basic validation
  • Side effects: Send queue messages, trigger webhooks, update caches
  • Audit logging: Track all changes with who, when, and what changed

For detailed examples and best practices, see Lifecycle Hooks Guide.

import { createRouter, RouterSettings } from '@verisure-italy/router-middleware'
import { User } from '@verisure-italy/aaa-types'

const settings: RouterSettings<{ user: User }> = {
  dataModels: {
    user: {
      tableName: 'users',
      hooks: {
        // Pre-hooks: modify data before saving
        preCreate: async (entity, req) => ({
          ...entity,
          createdAt: Date.now(),
          createdBy: req.auth?.user?.username,
          status: entity.status || 'active',
        }),
        
        // Post-hooks: trigger side effects after saving
        postCreate: async (entity, req) => {
          await sendWelcomeEmail(entity.email)
          await publishToQueue('user-created', { userId: entity.id })
        },
        
        // Validate business rules in pre-hooks
        preUpdate: async (updateData, existingEntity, req) => {
          if (updateData.email && existingEntity.emailVerified) {
            throw new Error('Cannot change verified email')
          }
          return {
            ...updateData,
            updatedAt: Date.now(),
            updatedBy: req.auth?.user?.username,
          }
        },
        
        // Track changes in post-hooks
        postUpdate: async (updated, previous, req) => {
          await logAuditTrail({
            action: 'user-updated',
            userId: updated.id,
            changes: getChanges(previous, updated),
            by: req.auth?.user?.username,
          })
        },
        
        // Prevent deletion with business logic
        preDelete: async (entity, req) => {
          if (entity.roles?.includes('ROLE_ADMIN')) {
            throw new Error('Cannot delete admin users')
          }
        },
        
        // Cleanup after deletion
        postDelete: async (entity, req) => {
          await deleteUserSessions(entity.id)
          await publishToQueue('user-deleted', { userId: entity.id })
        },
      },
    },
  },
}

Available Hooks:

  • preCreate: Modify entity before creation
  • postCreate: Execute after creation (e.g., send notifications)
  • preUpdate: Modify update data, access existing entity
  • postUpdate: Execute after update, compare old vs new
  • preDelete: Validate before deletion, can prevent it
  • postDelete: Cleanup after deletion

See the Configuration Reference below for detailed hook signatures and more examples.

Filtering, Sorting and Pagination

The router middleware now supports advanced filtering, sorting, and pagination for list operations. See the dedicated guides:

Quick example:

const settings: RouterSettings<{ user: User }> = {
  dataModels: {
    user: {
      tableName: 'users',
      queryConfig: {
        filterableFields: {
          status: { operators: ['=', 'in'] },
          email: { operators: ['=', 'begins_with', 'contains'] },
          age: { operators: ['=', '<', '<=', '>', '>=', 'between'] },
        },
        sortableFields: ['createdAt', 'updatedAt'],
        defaultSort: { field: 'createdAt', direction: 'desc' },
        defaultPageSize: 25,
      },
    },
  },
}

// API usage:
// GET /users?filter[status][=]=active&filter[age][>=]=18&sort=-createdAt&pageSize=50

Type-Safe Projection Fields

The projectionFields middleware now supports generics for complete type safety:

import { AccessToken } from '@verisure-italy/aaa-types'
import { requestProjectionFields } from '@verisure-italy/router-middleware'

// ✅ TypeScript will autocomplete and validate field names!
const projectionMiddleware = requestProjectionFields<AccessToken>([
  'id',
  'token', 
  'user',
  'expires'
  // ❌ TypeScript error if you add a field that doesn't exist in AccessToken
])

// Use in route
{
  method: 'get',
  path: '/access-tokens/:accessToken',
  handler: [projectionMiddleware, restControllers.read<AccessToken>],
  options: {
    dataModel: 'accessToken'
  }
}

Type-Safe Entity Enrichment

Enrich entities with additional data while maintaining type safety:

import { AccessToken, User } from '@verisure-italy/aaa-types'
import { entityEnrichment } from '@verisure-italy/router-middleware'

// ✅ Create type-safe enrichment
const enrichAccessToken = entityEnrichment<AccessToken>({
  accessToken: async (token: AccessToken, req) => {
    // Load user details
    const userDetails = await getUserDetails(token.user)
    
    // ✅ Return type must match AccessToken structure
    return {
      ...token,
      userDetails // Additional data
    }
  }
})

// Use in route
{
  method: 'get',
  path: '/access-tokens/:accessToken',
  handler: restControllers.read<AccessToken>,
  options: {
    dataModel: 'accessToken',
    // Or inline:
    entityEnrichment: {
      accessToken: async (token: AccessToken) => {
        const user = await getUserById(token.user)
        return { ...token, userDetails: user }
      }
    }
  }
}

Data Transformation

Transform data before validation:

{
  method: 'post',
  path: '/access-tokens',
  handler: restControllers.create<AccessToken>,
  options: {
    dataModel: 'accessToken',
    dataTransformer: {
      body: async (body) => {
        // Add expiration time automatically
        return {
          ...body,
          expires: Math.floor(Date.now() / 1000) + 3600 // 1 hour
        }
      }
    },
    validate: {
      body: accessTokenSchema
    }
  }
}

Pre-Authentication

Custom authentication logic:

{
  method: 'get',
  path: '/internal/tokens',
  handler: restControllers.list<AccessToken>,
  options: {
    dataModel: 'accessToken',
    // ✅ Custom authentication logic
    preAuth: async (req) => {
      const apiKey = req.headers['x-api-key']
      return apiKey === process.env.INTERNAL_API_KEY
    }
  }
}

Custom Authorization Handler

Complex authorization logic with type safety:

{
  method: 'delete',
  path: '/access-tokens/:accessToken',
  handler: restControllers.deleteEntity<AccessToken>,
  options: {
    dataModel: 'accessToken',
    secure: true,
    acl: {
      allow: {
        roles: ['ROLE_AAA_ADMIN']
      },
      // ✅ Custom authorization logic
      handler: async (auth, req) => {
        const token: AccessToken = req.accessToken
        
        // Allow only if user is deleting their own token or is admin
        return auth.user.username === token.user || 
               auth.user.roles.includes('ROLE_AAA_ADMIN')
      }
    }
  }
}

Type-Safe REST Controllers

All REST controllers are now generic and provide full type inference:

import { restControllers } from '@verisure-italy/router-middleware'
import { AccessToken } from '@verisure-italy/aaa-types'

// ✅ All controllers accept a generic type parameter
restControllers.create<AccessToken>     // POST - Create entity
restControllers.read<AccessToken>       // GET - Read single entity
restControllers.update<AccessToken>     // PUT - Update entity
restControllers.list<AccessToken>       // GET - List entities
restControllers.deleteEntity<AccessToken> // DELETE - Delete entity

// The generic type provides:
// - Type-safe repository operations
// - Correct field inference
// - Autocomplete for all entity properties

Creating Typed Controllers (Recommended)

To avoid repeating the generic type on every controller, use createTypedControllers:

import { AccessToken } from '@verisure-italy/aaa-types'
import { createTypedControllers, createRouter } from '@verisure-italy/router-middleware'

// ✅ Create typed controllers once for your entity
const accessTokenCtrl = createTypedControllers<AccessToken>()

// ✅ Now use them without repeating the generic type!
const router = createRouter<DataModels>([
  {
    method: 'get',
    path: '/access-tokens/:accessToken',
    handler: accessTokenCtrl.read,  // No need for <AccessToken>!
    options: {
      dataModel: 'accessToken',
      projectionFields: ['id', 'token', 'user', 'expires']
    }
  },
  {
    method: 'post',
    path: '/access-tokens',
    handler: accessTokenCtrl.create,  // Clean and simple!
    options: {
      dataModel: 'accessToken',
      validate: { body: accessTokenSchema }
    }
  },
  {
    method: 'put',
    path: '/access-tokens/:accessToken',
    handler: accessTokenCtrl.update,  // Type-safe!
    options: {
      dataModel: 'accessToken'
    }
  },
  {
    method: 'get',
    path: '/access-tokens',
    handler: accessTokenCtrl.list,
    options: {
      dataModel: 'accessToken'
    }
  },
  {
    method: 'delete',
    path: '/access-tokens/:accessToken',
    handler: accessTokenCtrl.delete,
    options: {
      dataModel: 'accessToken'
    }
  }
])

// You can create controllers for different entities
const userCtrl = createTypedControllers<User>()
const clientCtrl = createTypedControllers<Client>()

Benefits:

  • ✅ Define the type once per entity
  • ✅ Reuse controllers throughout your routes
  • ✅ Cleaner, more readable code
  • ✅ Fully type-safe with all the advantages

REST Resource Generation

For standard REST APIs without too many customizations, use createRestResource to automatically generate all CRUD routes with a simple configuration.

Basic REST Resource

import { AccessToken } from '@verisure-italy/aaa-types'
import { createRestResource, createRouter } from '@verisure-italy/router-middleware'

// ✅ One declaration creates all 5 routes
const routes = createRestResource<AccessToken>({
  dataModel: 'accessToken',
  basePath: '/access-tokens',
  sharedOptions: {
    secure: true,
    acl: {
      allow: { roles: ['ROLE_AAA_ADMIN'] }
    }
  }
})

// Automatically generates:
// POST   /access-tokens              → create
// GET    /access-tokens/:accessToken → read
// PUT    /access-tokens/:accessToken → update
// GET    /access-tokens              → list
// DELETE /access-tokens/:accessToken → delete

Select Specific Methods

// ✅ Generate only read and list (no create, update, delete)
const userRoutes = createRestResource<User>({
  dataModel: 'user',
  basePath: '/users',
  methods: ['read', 'list'], // Only these methods
  sharedOptions: {
    secure: true,
    projectionFields: ['id', 'username', 'roles']
  }
})

// Only creates:
// GET /users/:user → read
// GET /users       → list

Method-Specific Options

Use methodOptions to override shared options for specific methods:

const accessTokenRoutes = createRestResource<AccessToken>({
  dataModel: 'accessToken',
  basePath: '/access-tokens',
  
  // ✅ Shared options apply to ALL routes
  sharedOptions: {
    secure: true,
    acl: {
      allow: { roles: ['ROLE_AAA_ADMIN'] }
    }
  },
  
  // ✅ Method-specific options override shared options
  methodOptions: {
    read: {
      projectionFields: ['id', 'token', 'user', 'expires']
    },
    list: {
      // Override ACL: list also allows READER role
      acl: {
        allow: { roles: ['ROLE_AAA_ADMIN', 'ROLE_AAA_READER'] }
      }
    },
    create: {
      validate: {
        body: z.object({
          token: z.string().min(1),
          user: z.string(),
          client: z.object({ id: z.string() }),
          expires: z.number().positive(),
          scope: z.string().optional()
        })
      }
    },
    update: {
      validate: {
        body: z.object({
          expires: z.number().positive().optional(),
          scope: z.string().optional()
        })
      }
    }
  }
})

Batch Create Multiple Resources

Use createRestResources to generate routes for multiple entities at once:

import { createRestResources, createRouter } from '@verisure-italy/router-middleware'
import { AccessToken, User, Client } from '@verisure-italy/aaa-types'

// ✅ Create multiple REST resources in one call
const allRoutes = createRestResources([
  // Access Tokens - Full CRUD
  {
    dataModel: 'accessToken',
    basePath: '/access-tokens',
    sharedOptions: {
      secure: true,
      acl: { allow: { roles: ['ROLE_AAA_ADMIN'] } }
    },
    methodOptions: {
      read: {
        projectionFields: ['id', 'token', 'user', 'expires', 'scope']
      },
      list: {
        acl: { allow: { roles: ['ROLE_AAA_ADMIN', 'ROLE_AAA_READER'] } }
      }
    }
  },
  
  // Users - Read-only
  {
    dataModel: 'user',
    basePath: '/users',
    methods: ['read', 'list'],
    sharedOptions: {
      secure: true,
      acl: { allow: { roles: ['ROLE_AAA_ADMIN', 'ROLE_AAA_READER'] } }
    }
  },
  
  // Clients - Full CRUD with validation
  {
    dataModel: 'client',
    basePath: '/clients',
    sharedOptions: {
      secure: true,
      acl: { allow: { roles: ['ROLE_AAA_ADMIN'] } }
    },
    methodOptions: {
      create: {
        validate: {
          body: z.object({
            id: z.string(),
            grants: z.array(z.string()).min(1),
            redirectUris: z.array(z.url())
          })
        }
      }
    }
  }
])

const router = createRouter<DataModels>(allRoutes, {
  dataModels: {
    accessToken: { tableName: 'access_token', idField: 'id' },
    user: { tableName: 'user', idField: 'id' },
    client: { tableName: 'client', idField: 'id' }
  }
})

Complete Real-World Example

import { AccessToken, User, Client } from '@verisure-italy/aaa-types'
import { createRestResources, createRouter } from '@verisure-italy/router-middleware'
import { z } from 'zod'

interface DataModels {
  accessToken: AccessToken
  user: User
  client: Client
}

const routes = createRestResources([
  // Access Tokens - Full CRUD with admin access
  {
    dataModel: 'accessToken',
    basePath: '/access-tokens',
    sharedOptions: {
      secure: true,
      acl: { allow: { roles: ['ROLE_AAA_ADMIN'] } }
    },
    methodOptions: {
      read: {
        projectionFields: ['id', 'token', 'user', 'expires', 'scope'],
        entityEnrichment: {
          accessToken: async (token: AccessToken) => {
            const userDetails = await getUserById(token.user)
            return { ...token, userDetails }
          }
        }
      },
      list: {
        acl: { allow: { roles: ['ROLE_AAA_ADMIN', 'ROLE_AAA_READER'] } }
      },
      create: {
        validate: {
          body: z.object({
            token: z.string().min(1),
            user: z.string(),
            client: z.object({ id: z.string() }),
            expires: z.number().positive(),
            scope: z.string().optional()
          })
        },
        dataTransformer: {
          body: async (body) => ({
            ...body,
            // Auto-generate token if not provided
            token: body.token || generateSecureToken()
          })
        }
      }
    }
  },
  
  // Users - Read-only with projection
  {
    dataModel: 'user',
    basePath: '/users',
    methods: ['read', 'list'],
    sharedOptions: {
      secure: true,
      acl: { allow: { roles: ['ROLE_AAA_ADMIN', 'ROLE_AAA_READER'] } },
      projectionFields: ['id', 'username', 'roles']
    }
  },
  
  // Clients - Full CRUD with validation
  {
    dataModel: 'client',
    basePath: '/clients',
    sharedOptions: {
      secure: true,
      acl: { allow: { roles: ['ROLE_AAA_ADMIN'] } }
    },
    methodOptions: {
      create: {
        validate: {
          body: z.object({
            id: z.string(),
            grants: z.array(z.string()).min(1),
            refreshTokenLifetime: z.number().positive().nullable(),
            accessTokenLifetime: z.number().positive().nullable(),
            redirectUris: z.array(z.url())
          })
        }
      },
      update: {
        validate: {
          body: z.object({
            grants: z.array(z.string()).min(1).optional(),
            refreshTokenLifetime: z.number().positive().nullable().optional(),
            accessTokenLifetime: z.number().positive().nullable().optional(),
            redirectUris: z.array(z.url()).optional()
          })
        }
      }
    }
  }
])

export default createRouter<DataModels>(routes, {
  dataModels: {
    accessToken: { tableName: 'access_token', idField: 'id' },
    user: { tableName: 'user', idField: 'id' },
    client: { tableName: 'client', idField: 'id' }
  },
  dynamoConfig: {
    region: 'eu-west-1'
  }
})

Route Configuration Options

Main Route Configuration

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | method | 'get' \| 'post' \| 'put' \| 'patch' \| 'delete' | ✅ Yes | - | HTTP method for the route | | path | string | ✅ Yes | - | Route path (e.g., /access-tokens/:id) | | handler | RequestHandler \| RequestHandler[] | ✅ Yes | - | Express request handler(s) | | options | RouteOptions<TEntity> | ❌ No | {} | Additional route options (see below) |

Route Options (RouteOptions)

Security Options

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | secure | boolean | ❌ No | false | Require authentication (checks req._auth) | | preAuth | (req: Request) => Promise<boolean> \| boolean | ❌ No | - | Custom authentication logic executed before standard auth | | acl | AclConfig | ❌ No | - | Role-based access control configuration |

ACL Configuration (AclConfig)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | allow.roles | UserRole[] | ❌ No | [] | Array of roles allowed to access this route | | deny.roles | UserRole[] | ❌ No | [] | Array of roles explicitly denied access | | handler | (auth: AuthInfo, req: Request) => Promise<boolean> \| boolean | ❌ No | - | Custom authorization logic with access to auth info |

Validation Options

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | validate | ValidationConfig | ❌ No | - | Zod schema validation configuration |

Validation Configuration (ValidationConfig)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | body | z.ZodSchema | ❌ No | - | Zod schema for request body validation | | query | z.ZodSchema | ❌ No | - | Zod schema for query parameters validation | | params | z.ZodSchema | ❌ No | - | Zod schema for URL parameters validation |

Data Handling Options

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | dataModel | string | ❌ No | - | Name of the data model (must exist in RouterSettings.dataModels) | | projectionFields | (keyof TEntity)[] | ❌ No | - | Array of entity fields to return in response (type-safe) | | dataTransformer | DataTransformerConfig | ❌ No | - | Transform request data before validation | | entityEnrichment | TypedEntityEnrichmentConfig<TEntity> | ❌ No | - | Enrich entity with additional data (type-safe) |

Data Transformer Configuration (DataTransformerConfig)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | body | (data: any, req: Request) => Promise<any> \| any | ❌ No | - | Transform request body |

Note: query and params transformation has been removed in favor of Express 5 compliance. Use Zod transforms in validation for query/params manipulation.

Entity Enrichment Configuration (TypedEntityEnrichmentConfig)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | [key: string] | (entity: TEntity, req: Request) => Promise<TEntity> \| TEntity | ❌ No | - | Function to enrich the entity (key matches entity name) |

Advanced Options

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | filterable | FilterableConfig | ❌ No | - | Configuration for list filtering and pagination | | cache | CacheConfig | ❌ No | - | Response caching configuration | | disableWebhook | boolean | ❌ No | false | Disable webhook trigger for this route |

Filterable Configuration (FilterableConfig)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | filtersMap | Record<string, FilterMapConfig> | ✅ Yes | - | Map of available filters | | defaultFilters | Record<string, any> \| ((req: Request) => Record<string, any>) | ❌ No | - | Default filters to apply | | pageSize | number | ❌ No | - | Default page size for pagination | | filterParser | (filter: any) => any | ❌ No | - | Custom filter parsing function |

Filter Map Configuration (FilterMapConfig)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | name | string | ✅ Yes | - | Filter field name in DynamoDB | | condition | string | ✅ Yes | - | Filter condition (e.g., '=', '>', '<') | | secondary | string | ❌ No | - | Secondary filter field for range queries |

Cache Configuration (CacheConfig)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | ttl | string | ✅ Yes | - | Time to live (e.g., '5 minutes', '1 hour') |

Router Settings Configuration

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | dynamoConfig | Partial<DynamoConfig> | ❌ No | - | DynamoDB client configuration | | dataModels | { [K in keyof TDataModels]: EntityConfig<TDataModels[K]> } | ❌ No | {} | Entity/data model definitions | | webhook | WebhookConfig | ❌ No | - | Webhook configuration | | keepalive | KeepaliveConfig | ❌ No | { enabled: true } | Keepalive/health check endpoint |

DynamoDB Configuration (DynamoConfig)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | region | string | ❌ No | 'eu-west-1' | AWS region | | endpoint | string | ❌ No | - | Custom DynamoDB endpoint (for local development) | | tablePrefix | string | ❌ No | '' | Prefix for table names | | credentials | AwsCredentialIdentity | ❌ No | - | AWS credentials |

Entity Configuration (EntityConfig)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | tableName | string | ✅ Yes | - | DynamoDB table name | | idField | keyof TEntity & string | ❌ No | 'id' | Name of the ID field (type-safe) | | indexes | Record<string, IndexConfig<TEntity>> | ❌ No | {} | DynamoDB index configurations | | generateId | (entity: Partial<TEntity>) => string \| Promise<string> | ❌ No | UUID v7 generator | Custom ID generator function (receives entity data, returns unique ID) | | hooks | EntityHooks<TEntity> | ❌ No | {} | Lifecycle hooks for entity operations (preCreate, postCreate, preUpdate, postUpdate, preDelete, postDelete) |

Note: If generateId is not specified, UUID v7 (time-ordered UUID) is used by default. See ID Generation Configuration for detailed examples.

Index Configuration (IndexConfig)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | hashKey | keyof TEntity & string | ✅ Yes | - | Hash key field name (type-safe) | | rangeKey | keyof TEntity & string | ❌ No | - | Range key field name (type-safe) |

Lifecycle Hooks (EntityHooks)

Lifecycle hooks allow you to execute custom logic before and after entity operations (create, update, delete). This is useful for:

  • Modifying entity data before saving (e.g., adding timestamps, normalizing data)
  • Validating business rules
  • Sending messages to queues
  • Triggering side effects (e.g., sending emails, updating caches)
  • Logging changes

| Hook | Type | Description | |------|------|-------------| | preCreate | (entity: Partial<TEntity>, req: Request) => Promise<Partial<TEntity>> \| Partial<TEntity> | Executed before creating an entity. Can modify the entity data before it's saved. | | postCreate | (entity: TEntity, req: Request) => Promise<void> \| void | Executed after creating an entity. Useful for side effects like sending messages to a queue. | | preUpdate | (entity: Partial<TEntity>, existingEntity: TEntity, req: Request) => Promise<Partial<TEntity>> \| Partial<TEntity> | Executed before updating an entity. Can modify the update data before it's saved. Receives both the update data and the current entity. | | postUpdate | (entity: TEntity, previousEntity: TEntity, req: Request) => Promise<void> \| void | Executed after updating an entity. Receives both the updated entity and the previous state. | | preDelete | (entity: TEntity, req: Request) => Promise<void> \| void | Executed before deleting an entity. Can prevent deletion by throwing an error. | | postDelete | (entity: TEntity, req: Request) => Promise<void> \| void | Executed after deleting an entity. Useful for cleanup or sending messages. |

Example: Using Lifecycle Hooks

import { createRouter, RouterSettings } from '@verisure-italy/router-middleware'
import { User } from '@verisure-italy/aaa-types'

interface DataModels {
  user: User
}

const settings: RouterSettings<DataModels> = {
  dataModels: {
    user: {
      tableName: 'users',
      idField: 'id',
      hooks: {
        // Add timestamps before creating
        preCreate: async (entity, req) => {
          const now = Date.now()
          return {
            ...entity,
            createdAt: now,
            updatedAt: now,
            createdBy: req.auth?.user?.username || 'system',
          }
        },
        
        // Send welcome email after creating
        postCreate: async (entity, req) => {
          await sendWelcomeEmail(entity.email)
          await publishToQueue('user-created', { userId: entity.id })
          console.log(`User created: ${entity.id} by ${req.auth?.user?.username}`)
        },
        
        // Update timestamp and validate before updating
        preUpdate: async (updateData, existingEntity, req) => {
          // Add updated timestamp
          const data = {
            ...updateData,
            updatedAt: Date.now(),
            updatedBy: req.auth?.user?.username || 'system',
          }
          
          // Business rule: prevent changing email if verified
          if (updateData.email && existingEntity.emailVerified) {
            throw new Error('Cannot change verified email')
          }
          
          return data
        },
        
        // Log changes and notify after updating
        postUpdate: async (updatedEntity, previousEntity, req) => {
          await logAuditTrail({
            action: 'user-updated',
            userId: updatedEntity.id,
            changes: getChanges(previousEntity, updatedEntity),
            by: req.auth?.user?.username,
          })
          
          // Notify if status changed
          if (updatedEntity.status !== previousEntity.status) {
            await publishToQueue('user-status-changed', {
              userId: updatedEntity.id,
              oldStatus: previousEntity.status,
              newStatus: updatedEntity.status,
            })
          }
        },
        
        // Prevent deletion of admin users
        preDelete: async (entity, req) => {
          if (entity.roles?.includes('ROLE_AAA_ADMIN')) {
            throw new Error('Cannot delete admin users')
          }
          
          // Soft delete by updating status instead
          if (!req.query.force) {
            throw new Error('Use force=true to permanently delete')
          }
        },
        
        // Cleanup and notify after deletion
        postDelete: async (entity, req) => {
          // Delete related data
          await deleteUserSessions(entity.id)
          await deleteUserTokens(entity.id)
          
          // Notify systems
          await publishToQueue('user-deleted', { userId: entity.id })
          
          // Log for audit
          console.log(`User deleted: ${entity.id} by ${req.auth?.user?.username}`)
        },
      },
    },
  },
  dynamoConfig: {
    region: 'eu-west-1',
  },
}

const router = createRouter<DataModels>(routes, settings)

Example: Modifying Data in Pre-Hooks

const settings: RouterSettings<DataModels> = {
  dataModels: {
    product: {
      tableName: 'products',
      hooks: {
        preCreate: async (entity) => {
          return {
            ...entity,
            // Generate SKU automatically
            sku: generateSKU(entity.name, entity.category),
            // Normalize name
            name: entity.name?.trim().toLowerCase(),
            // Add slug
            slug: slugify(entity.name),
            // Set default values
            status: entity.status || 'draft',
            stock: entity.stock || 0,
          }
        },
        
        preUpdate: async (updateData, existingEntity) => {
          const data = { ...updateData }
          
          // Recalculate slug if name changed
          if (data.name && data.name !== existingEntity.name) {
            data.slug = slugify(data.name)
          }
          
          // Auto-update status based on stock
          if (data.stock !== undefined) {
            data.status = data.stock > 0 ? 'available' : 'out-of-stock'
          }
          
          return data
        },
      },
    },
  },
}

Example: Queue Integration in Post-Hooks

import { SQS } from '@aws-sdk/client-sqs'

const sqs = new SQS({ region: 'eu-west-1' })

const settings: RouterSettings<DataModels> = {
  dataModels: {
    order: {
      tableName: 'orders',
      hooks: {
        postCreate: async (order) => {
          // Send to order processing queue
          await sqs.sendMessage({
            QueueUrl: process.env.ORDER_QUEUE_URL,
            MessageBody: JSON.stringify({
              type: 'order-created',
              orderId: order.id,
              customerId: order.customerId,
              amount: order.total,
            }),
          })
        },
        
        postUpdate: async (order, previousOrder) => {
          // Notify if order status changed
          if (order.status !== previousOrder.status) {
            await sqs.sendMessage({
              QueueUrl: process.env.NOTIFICATION_QUEUE_URL,
              MessageBody: JSON.stringify({
                type: 'order-status-changed',
                orderId: order.id,
                customerId: order.customerId,
                oldStatus: previousOrder.status,
                newStatus: order.status,
              }),
            })
          }
        },
      },
    },
  },
}

Webhook Configuration (WebhookConfig)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | enabled | boolean | ✅ Yes | - | Enable webhook functionality | | path | string | ❌ No | '/webhooks' | Webhook endpoint path | | handler | RequestHandler | ❌ No | - | Custom webhook handler | | queue | string | ❌ No | - | Queue name for webhook events | | service | string | ❌ No | - | Service name for webhook events | | triggers | WebhookTrigger[] | ❌ No | [] | Array of webhook triggers |

Webhook Trigger Configuration (WebhookTrigger)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | entity | string | ✅ Yes | - | Entity name that triggers webhook | | events | ('create' \| 'update' \| 'delete')[] | ✅ Yes | - | Array of events that trigger webhook | | endpoints | string[] | ✅ Yes | - | Array of webhook endpoint URLs |

Keepalive Configuration (KeepaliveConfig)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | enabled | boolean | ❌ No | true | Enable keepalive/health check endpoint | | path | string | ❌ No | '/keepalive' | Keepalive endpoint path | | handler | (req: Request, res: Response) => Promise<void> \| void | ❌ No | - | Custom keepalive handler |

REST Resource Configuration

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | dataModel | string | ✅ Yes | - | Data model name (must exist in RouterSettings) | | basePath | string | ✅ Yes | - | Base path for all routes (e.g., /access-tokens) | | methods | ('create' \| 'read' \| 'update' \| 'list' \| 'delete')[] | ❌ No | ['create', 'read', 'update', 'list', 'delete'] | Which REST methods to generate | | sharedOptions | RouteOptions<TEntity> | ❌ No | {} | Options applied to ALL generated routes | | methodOptions | { create?, read?, update?, list?, delete?: RouteOptions<TEntity> } | ❌ No | {} | Options specific to each method (override shared) |

Type Reference

UserRole: Imported from @verisure-italy/aaa-types

type UserRole = 
  | 'ROLE_AAA_ADMIN'
  | 'ROLE_AAA_READER'
  | 'ROLE_AAA_WRITER'
  // ... other roles defined in aaa-types

AuthInfo: Authentication information

interface AuthInfo {
  user: {
    id: string
    username: string
    roles: UserRole[]
    [key: string]: any
  }
  token: {
    accessToken: string
    accessTokenExpiresAt: string
    scope: string[]
  }
}

RequestHandler: Express request handler

type RequestHandler = (
  req: Request,
  res: Response,
  next: NextFunction
) => void | Promise<void>

Route Configuration

interface RouteConfig<TPath, TEntity> {
  method: 'get' | 'post' | 'put' | 'patch' | 'delete'
  path: TPath
  handler: RequestHandler | RequestHandler[]
  options?: RouteOptions<TEntity>
}

Route Options

interface RouteOptions<TEntity> {
  // Security
  secure?: boolean                    // Require authentication
  preAuth?: PreAuthHandler            // Custom auth logic
  acl?: AclConfig                     // Role-based access control
  
  // Validation
  validate?: ValidationConfig         // Zod schemas for body/query/params
  
  // Data handling
  dataModel?: string                  // Data model name
  projectionFields?: (keyof TEntity)[] // ✅ Type-safe field selection
  dataTransformer?: DataTransformerConfig
  entityEnrichment?: TypedEntityEnrichmentConfig<TEntity> // ✅ Type-safe enrichment
  
  // Advanced
  filterable?: FilterableConfig       // List filtering
  cache?: CacheConfig                 // Response caching
  disableWebhook?: boolean           // Disable webhook trigger
}