Skip to main content

User Guide

Table of Contents


App Initialization

initApp creates a pre-configured Express application with security middleware, CORS, compression, and a /health endpoint.

import { initApp } from '@elliemae/pui-service-sdk';

// Use defaults (ICE/Ellie Mae CORS origins)
const app = initApp();

// Or provide custom options
const app = initApp({
corsOptions: {
origin: ['https://my-app.example.com'],
methods: 'GET,POST',
},
jsonOptions: {
limit: '1mb',
strict: true,
},
});

Default CORS Configuration

The built-in CORS policy allows requests from these domains:

  • *.ice.com
  • *.elliemae.com
  • *.elliemae.io
  • *.ellieservices.com
  • *.ellielabs.com

Allowed headers: Authorization, Content-Type, Origin, X-Requested-With, Accept

Default JSON Options

  • Body size limit: 100kb
  • Content type: application/json

Logger

The SDK provides a structured Pino logger with automatic PII redaction, environment metadata, and log record validation.

Creating a Logger

import { logger } from '@elliemae/pui-service-sdk';

const log = logger({
appName: 'my-service', // required
team: 'my-team', // required
environment: 'production', // optional, falls back to ENV env var
appVersion: '1.2.0', // optional, falls back to VERSION env var
location: 'us-west-2', // optional, falls back to LOCATION env var
index: 'my-index', // optional, log index identifier
level: 'debug', // optional, default: 'info'
});

Logging Messages

Every structured log message requires message and correlationId:

log.info({
message: 'Order created successfully',
correlationId: 'abc-123-def',
resourceType: 'order',
resourceId: '12345',
});

You can also pass a plain string (a correlationId is generated automatically):

log.info('Server started');

Logging Errors

Pass errors using the exception field:

log.error({
message: 'Failed to process order',
correlationId: 'abc-123-def',
errorCode: 'ORD001',
exception: new Error('Database timeout'),
});

PII Redaction

The logger automatically redacts sensitive data from all log output:

  • Field-level redaction: Fields named password, pwd, creditcard, credentials, ssn, or socialsecuritynumber are replaced with [Redacted]
  • Pattern-based redaction: Credit card numbers, SSNs, email addresses, street addresses, IP addresses, and credential patterns within string values are detected and replaced

Environment Variables

The logger reads these environment variables at initialization:

VariableMaps toFallback
ENVenvironmentOptions value or 'NA'
VERSIONappVersionOptions value or 'NA'
LOCATIONlocationOptions value or 'NA'

Routes and Error Handling

Use service.handleRequest to wire routes and error handling onto your app:

import { initApp, service, logger } from '@elliemae/pui-service-sdk';

const log = logger({ appName: 'my-service', team: 'my-team' });

const userRoutes = (app) => {
app.get('/api/users', (req, res) => {
res.json([{ id: 1, name: 'Jane' }]);
});

app.get('/api/users/:id', (req, res) => {
res.json({ id: req.params.id, name: 'Jane' });
});
};

const orderRoutes = (app) => {
app.post('/api/orders', (req, res) => {
res.status(201).json({ id: 'new-order' });
});
};

const app = initApp();
service.handleRequest({
logger: log,
routes: [userRoutes, orderRoutes],
})(app);

Built-in Error Handling

After all routes are registered, handleRequest automatically adds:

  1. 404 handler — Returns "endpoint not found" for unmatched routes
  2. Unhandled error handler — Catches thrown exceptions, logs them with a correlation ID (puisvc001), and returns an error response:
    • 5xx errors — Returns "Internal Server Error" (hides internal details from clients)
    • 4xx errors — Returns the actual error message for easier client-side debugging

Starting the Server

Use listen to start the HTTP server with graceful shutdown support:

import { listen } from '@elliemae/pui-service-sdk';

// Default port: process.env.PORT or 3000
const server = await listen(app);

// Custom port with cleanup hook
const server = await listen(app, {
port: 8080,
onClose: async () => {
await database.disconnect();
await cache.quit();
},
});

Error Handling

The returned Promise rejects if the server fails to start (e.g., port already in use):

try {
const server = await listen(app, { port: 8080 });
} catch (err) {
console.error('Server failed to start:', err.message);
process.exit(1);
}

Graceful Shutdown

When the process receives SIGINT or SIGTERM:

  1. The server stops accepting new connections
  2. Existing connections are allowed to complete
  3. The onClose callback (if provided) is awaited for cleanup
  4. The process exits

Serverless Deployment

Wrap your Express app for AWS Lambda:

import { initApp, service, logger, serverless } from '@elliemae/pui-service-sdk';

const log = logger({ appName: 'my-lambda', team: 'my-team' });

const routes = (app) => {
app.get('/api/data', (req, res) => {
res.json({ data: 'from lambda' });
});
};

const app = initApp();
service.handleRequest({ logger: log, routes: [routes] })(app);

export const handler = serverless(app);

This uses @vendia/serverless-express under the hood to translate API Gateway events into Express requests. The serverless wrapper automatically uses the logger configured via service.handleRequest (from app.locals.logger), so your Lambda logs retain the same structured format, PII redaction, and metadata as your local server. If no logger is configured, it falls back to a default Pino instance.


API Requests

service.apiRequest provides an HTTP client with automatic retries and structured error handling:

const { apiRequest, APIError } = service;

try {
const data = await apiRequest<{ users: User[] }>({
url: 'https://api.example.com/users',
options: {
method: 'GET',
headers: {
Authorization: 'Bearer my-token',
'X-Correlation-ID': 'abc-123',
},
},
loggerService: log,
});
} catch (error) {
if (error instanceof APIError) {
console.error(error.details.responseStatusCode);
}
}

Options

ParameterTypeDefaultDescription
urlstringRequest URL (required)
optionsRequestInitFetch options: method, headers, body (required)
retryDelaynumber500Base delay between retries (ms)
maxAttemptsnumber3Maximum number of attempts
retryCodesnumber[][0, 408, 502, 503, 504]HTTP status codes that trigger retries
loggerServicepino.Logger | ConsoleconsoleLogger instance

Retry Behavior

  • Retries use linear backoff: attempt * retryDelay
  • Only status codes in retryCodes trigger retries
  • After exhausting all attempts, an APIError is thrown
  • Non-retryable error codes throw APIError immediately

Response Handling

  • application/json responses are parsed as JSON
  • text/plain responses are returned as strings
  • Other content types throw an APIError

Redis Cache Service

service.RedisService provides a Redis Cluster client with automatic fallback to an in-memory cache:

const { RedisService } = service;

const cache = new RedisService({
redisUrl: 'rediss://my-redis-cluster.example.com:6379',
namespace: 'my-service',
loggerService: log,
});

// Store a value (TTL in seconds)
await cache.set('user:123', JSON.stringify(userData), 300);

// Retrieve a value
const cached = await cache.get('user:123');
if (cached) {
const user = JSON.parse(cached);
}

Options

ParameterTypeDefaultDescription
redisUrlstringRedis cluster URL (required)
namespacestringKey prefix for cache isolation (required)
loggerServicepino.LoggerconsoleLogger instance
redisClientTimeoutnumber2000Connection timeout (ms)
redisClientMaxRetrynumber3Max retries per request
redisDefaultTTLnumber60Default TTL in seconds
defaultRedisPortnumber6379Fallback port if not in URL

Fallback Behavior

If Redis is unavailable (connection failure, timeout, etc.), the service transparently falls back to an in-memory cache. This ensures your service stays operational during Redis outages, though cached data will not be shared across instances.

Connection Management

The service manages its Redis Cluster connection automatically:

  • Connection deduplication — Concurrent requests during startup share a single connection attempt rather than creating duplicate clients
  • Automatic reconnection — If the Redis connection drops, the service detects the closure and establishes a new connection on the next get() or set() call
  • TLS support — Connections use TLS by default, compatible with AWS ElastiCache clusters

Key Namespacing

All keys are automatically prefixed with namespace: to prevent collisions between services sharing a Redis cluster.


Token Service

service.TokenService handles OAuth2 token retrieval and JWT verification with Redis-backed caching:

const { TokenService, RedisService } = service;

const cache = new RedisService({
redisUrl: process.env.REDIS_URL,
namespace: 'my-service',
});

const tokenService = new TokenService({
oapiUrl: 'https://api.elliemae.com',
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
redisCacheService: cache,
loggerService: log,
});

Getting a Token

const token = await tokenService.getJwt({
grant_type: 'client_credentials',
client_id: 'my-client',
client_secret: 'my-secret',
scope: 'read write',
roles: 'admin',
aud: 'my-audience',
elli_iid: 'instance-id',
realm: 'my-realm',
});

// token.access_token contains the JWT

Tokens are cached in Redis (default TTL: 240 seconds) to minimize OAuth calls.

Verifying a Token

const isValid = await tokenService.verifyJwt('Bearer eyJhbGci...');
// Returns true if signature is valid, throws TokenServiceError otherwise

Verification fetches JWKS from the OAuth provider and caches keys in Redis (default TTL: 24 hours).

Utility Methods

// Strip "Bearer " prefix
const jwt = tokenService.bearerTokenToJwt('Bearer eyJhbGci...');

// Add "Bearer " prefix
const bearer = tokenService.jwtToBearerToken('eyJhbGci...');

// Decode without verification
const decoded = tokenService.decodeJwt('Bearer eyJhbGci...');

Parameter Store Service

service.ParamStoreService retrieves parameters from AWS Systems Manager Parameter Store:

const { ParamStoreService } = service;

const paramStore = new ParamStoreService({
namespace: '/my-service/production/',
loggerService: log,
});

// Get a plain text parameter
const apiUrl = await paramStore.get('api-url');

// Get an encrypted parameter (SecureString)
const dbPassword = await paramStore.get('db-password', true);

The namespace is prepended to all parameter names, so paramStore.get('api-url') fetches /my-service/production/api-url from Parameter Store.


Request Validation

The SDK provides Express middleware for request validation using express-validator:

import { validate, customValidators } from '@elliemae/pui-service-sdk';
import { body, param } from 'express-validator';

const createUserRoute = (app) => {
app.post(
'/api/users',
validate([
body('email').isEmail(),
body('name').isString().notEmpty(),
body('roles').custom(customValidators.isArray),
body('roles').custom(customValidators.notEmpty),
]),
(req, res) => {
// Only reached if validation passes
res.status(201).json({ created: true });
},
);
};

Behavior

  • If all validations pass, the next middleware/handler is called
  • If any validation fails, a 400 response (or custom status code) is returned with the error details:
{
"errors": [
{
"type": "field",
"msg": "Invalid value",
"path": "email",
"location": "body"
}
]
}

Custom Status Code

validate([body('id').isUUID()], 422)  // Returns 422 on failure instead of 400

Built-in Custom Validators

ValidatorDescription
customValidators.isArray(value)Returns true if value is an array
customValidators.notEmpty(array)Returns true if array has at least one element