Express Middleware
This chapter covers advanced Express.js integration patterns with JACS, building on the basics covered in HTTP Server.
Overview
JACS provides JACSExpressMiddleware for seamless integration with Express.js applications:
- Automatic request verification
- Automatic response signing
- Access to verified payloads via
req.jacsPayload - Error handling for invalid requests
Quick Start
import express from 'express';
import { JACSExpressMiddleware } from 'jacsnpm/http';
const app = express();
// Required: Parse body as text before JACS middleware
app.use('/api', express.text({ type: '*/*' }));
// Apply JACS middleware
app.use('/api', JACSExpressMiddleware({
configPath: './jacs.config.json'
}));
// Routes automatically get verified payloads and signed responses
app.post('/api/data', (req, res) => {
const payload = req.jacsPayload;
res.send({ received: payload, status: 'ok' });
});
app.listen(3000);
Middleware Configuration
Basic Configuration
JACSExpressMiddleware({
configPath: './jacs.config.json' // Required: path to JACS config
})
Per-Route Configuration
Apply JACS to specific routes:
const app = express();
// Non-JACS routes (public endpoints)
app.get('/health', (req, res) => res.send({ status: 'ok' }));
app.get('/public/info', (req, res) => res.send({ name: 'My API' }));
// JACS-protected routes
app.use('/api', express.text({ type: '*/*' }));
app.use('/api', JACSExpressMiddleware({ configPath: './jacs.config.json' }));
app.post('/api/secure', (req, res) => {
// Only JACS-signed requests reach here
res.send({ data: 'secure response' });
});
Multiple JACS Agents
Use different JACS agents for different routes:
// Admin routes with admin agent
app.use('/admin', express.text({ type: '*/*' }));
app.use('/admin', JACSExpressMiddleware({
configPath: './jacs.admin.config.json'
}));
// User routes with user agent
app.use('/user', express.text({ type: '*/*' }));
app.use('/user', JACSExpressMiddleware({
configPath: './jacs.user.config.json'
}));
Request Handling
Accessing Verified Payload
The middleware attaches the verified payload to req.jacsPayload:
app.post('/api/process', (req, res) => {
// req.jacsPayload contains the verified, decrypted payload
const { action, data, timestamp } = req.jacsPayload;
console.log('Action:', action);
console.log('Data:', data);
console.log('Request timestamp:', timestamp);
res.send({ processed: true });
});
Handling Missing Payload
If JACS verification fails, req.jacsPayload will be undefined:
app.post('/api/secure', (req, res) => {
if (!req.jacsPayload) {
return res.status(400).json({ error: 'Invalid JACS request' });
}
// Process verified payload
res.send({ success: true });
});
Validation Helper
Create a reusable validation middleware:
function requireJacsPayload(req, res, next) {
if (!req.jacsPayload) {
return res.status(400).json({
error: 'JACS verification failed',
message: 'Request must be signed with valid JACS credentials'
});
}
next();
}
// Apply to routes
app.post('/api/secure', requireJacsPayload, (req, res) => {
// Guaranteed to have valid req.jacsPayload
res.send({ data: req.jacsPayload });
});
Response Handling
Automatic Signing
When you call res.send() with an object, the middleware automatically signs it:
app.post('/api/data', (req, res) => {
// This object will be automatically JACS-signed
res.send({
result: 'success',
data: { value: 42 },
timestamp: new Date().toISOString()
});
});
Sending Unsigned Responses
To bypass automatic signing, send a string directly:
app.post('/api/raw', (req, res) => {
// String responses are not signed
res.type('text/plain').send('Raw text response');
});
Custom Response Format
app.post('/api/custom', (req, res) => {
const response = {
success: true,
payload: {
action: 'completed',
result: processRequest(req.jacsPayload)
},
metadata: {
serverTime: new Date().toISOString(),
requestId: generateRequestId()
}
};
// Automatically signed before sending
res.send(response);
});
Error Handling
Global Error Handler
import express from 'express';
import { JACSExpressMiddleware } from 'jacsnpm/http';
const app = express();
app.use('/api', express.text({ type: '*/*' }));
app.use('/api', JACSExpressMiddleware({ configPath: './jacs.config.json' }));
app.post('/api/process', (req, res, next) => {
try {
if (!req.jacsPayload) {
throw new Error('Missing JACS payload');
}
const result = processData(req.jacsPayload);
res.send({ result });
} catch (error) {
next(error);
}
});
// Global error handler
app.use((error, req, res, next) => {
console.error('Error:', error.message);
res.status(500).send({
error: 'Internal server error',
message: error.message
});
});
Typed Errors
class JacsValidationError extends Error {
constructor(message) {
super(message);
this.name = 'JacsValidationError';
this.statusCode = 400;
}
}
app.post('/api/validate', (req, res, next) => {
try {
if (!req.jacsPayload) {
throw new JacsValidationError('Invalid JACS request');
}
const { requiredField } = req.jacsPayload;
if (!requiredField) {
throw new JacsValidationError('Missing required field');
}
res.send({ valid: true });
} catch (error) {
next(error);
}
});
// Error handler
app.use((error, req, res, next) => {
const statusCode = error.statusCode || 500;
res.status(statusCode).send({
error: error.name,
message: error.message
});
});
Advanced Patterns
Router-Level Middleware
import { Router } from 'express';
import { JACSExpressMiddleware } from 'jacsnpm/http';
// Create a JACS-enabled router
function createJacsRouter(configPath) {
const router = Router();
router.use(express.text({ type: '*/*' }));
router.use(JACSExpressMiddleware({ configPath }));
return router;
}
// Usage
const apiRouter = createJacsRouter('./jacs.config.json');
apiRouter.post('/users', (req, res) => {
res.send({ users: getUserList() });
});
apiRouter.post('/orders', (req, res) => {
res.send({ orders: getOrders(req.jacsPayload.userId) });
});
app.use('/api', apiRouter);
Middleware Composition
Combine JACS with other middleware:
import rateLimit from 'express-rate-limit';
import { JACSExpressMiddleware } from 'jacsnpm/http';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
// Apply multiple middleware in order
app.use('/api',
limiter, // Rate limiting first
express.text({ type: '*/*' }), // Parse body as text
JACSExpressMiddleware({ configPath: './jacs.config.json' }) // JACS verification
);
Logging Middleware
Log JACS requests for auditing:
function jacsLogger(req, res, next) {
if (req.jacsPayload) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
method: req.method,
path: req.path,
jacsPayload: req.jacsPayload,
ip: req.ip
}));
}
next();
}
app.use('/api', express.text({ type: '*/*' }));
app.use('/api', JACSExpressMiddleware({ configPath: './jacs.config.json' }));
app.use('/api', jacsLogger); // After JACS middleware
Authentication Integration
Combine JACS with user authentication:
// JACS middleware first
app.use('/api', express.text({ type: '*/*' }));
app.use('/api', JACSExpressMiddleware({ configPath: './jacs.config.json' }));
// Then authentication check
function requireAuth(req, res, next) {
const payload = req.jacsPayload;
if (!payload || !payload.userId) {
return res.status(401).send({ error: 'Authentication required' });
}
// Attach user to request
req.user = { id: payload.userId };
next();
}
app.post('/api/protected', requireAuth, (req, res) => {
res.send({
message: `Hello, user ${req.user.id}`,
data: req.jacsPayload.data
});
});
Testing
Unit Testing Routes
import request from 'supertest';
import jacs from 'jacsnpm';
describe('JACS API', () => {
beforeAll(async () => {
await jacs.load('./jacs.test.config.json');
});
it('should accept valid JACS requests', async () => {
const payload = { action: 'test', data: 'hello' };
const signedRequest = await jacs.signRequest(payload);
const response = await request(app)
.post('/api/echo')
.set('Content-Type', 'text/plain')
.send(signedRequest);
expect(response.status).toBe(200);
// Verify response is JACS-signed
const verified = await jacs.verifyResponse(response.text);
expect(verified.payload.echo).toEqual(payload);
});
it('should reject unsigned requests', async () => {
const response = await request(app)
.post('/api/echo')
.set('Content-Type', 'text/plain')
.send('{"invalid": "request"}');
expect(response.status).toBe(400);
});
});
Mock JACS for Testing
// test/mocks/jacs.js
export const mockJacs = {
payload: null,
setPayload(p) {
this.payload = p;
},
reset() {
this.payload = null;
}
};
// Mock middleware for testing
export function mockJacsMiddleware(req, res, next) {
req.jacsPayload = mockJacs.payload;
next();
}
// In tests
describe('API without real JACS', () => {
beforeEach(() => {
mockJacs.setPayload({ userId: 'test-user', action: 'test' });
});
afterEach(() => {
mockJacs.reset();
});
it('processes payload correctly', async () => {
const response = await request(testApp)
.post('/api/process')
.send('test');
expect(response.status).toBe(200);
});
});
Complete Application Example
import express from 'express';
import { JACSExpressMiddleware } from 'jacsnpm/http';
const app = express();
// Health check (no JACS)
app.get('/health', (req, res) => res.send({ status: 'healthy' }));
// JACS-protected API routes
app.use('/api', express.text({ type: '*/*' }));
app.use('/api', JACSExpressMiddleware({
configPath: './jacs.config.json'
}));
// Validation middleware
function requirePayload(req, res, next) {
if (!req.jacsPayload) {
return res.status(400).send({ error: 'Invalid JACS request' });
}
next();
}
// Routes
app.post('/api/echo', requirePayload, (req, res) => {
res.send({ echo: req.jacsPayload });
});
app.post('/api/users', requirePayload, (req, res) => {
const { name, email } = req.jacsPayload;
if (!name || !email) {
return res.status(400).send({ error: 'Name and email required' });
}
const user = createUser({ name, email });
res.send({ user, created: true });
});
app.post('/api/documents', requirePayload, async (req, res) => {
const { title, content } = req.jacsPayload;
const document = await createDocument({ title, content });
res.send({ document });
});
// Error handler
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(500).send({ error: 'Internal server error' });
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`JACS Express server listening on port ${PORT}`);
});
Troubleshooting
Body Parsing Issues
Problem: req.jacsPayload is always undefined
Solution: Ensure express.text() comes before JACS middleware:
// Correct
app.use('/api', express.text({ type: '*/*' }));
app.use('/api', JACSExpressMiddleware({ configPath: '...' }));
// Wrong
app.use('/api', JACSExpressMiddleware({ configPath: '...' }));
app.use('/api', express.text({ type: '*/*' }));
JSON Body Parser Conflict
Problem: Using express.json() interferes with JACS
Solution: Use route-specific middleware:
// JSON for non-JACS routes
app.use('/public', express.json());
// Text for JACS routes
app.use('/api', express.text({ type: '*/*' }));
app.use('/api', JACSExpressMiddleware({ configPath: '...' }));
Response Not Signed
Problem: Responses are plain JSON, not JACS-signed
Solution: Ensure you're sending an object, not a string:
// Will be signed
res.send({ data: 'value' });
// Will NOT be signed
res.send(JSON.stringify({ data: 'value' }));
Next Steps
- HTTP Server - Core HTTP integration concepts
- MCP Integration - Model Context Protocol support
- API Reference - Complete API documentation