Lightweight HTTP REST API framework with built-in hypermedia support
# npm
npm install tenso
# yarn
yarn add tenso
# pnpm
pnpm add tenso
import {tenso} from "tenso";
export const app = tenso();
app.get("/", "Hello, World!");
app.start();
import {tenso} from "tenso";
import {randomUUID as uuid} from "crypto";
const initRoutes = {
"get": {
"/": ["reports", "uuid"],
"/reports": ["tps"],
"/reports/tps": (req, res) => res.error(785, Error("TPS Cover Sheet not attached")),
"/uuid": (req, res) => res.send(uuid(), 200, {"cache-control": "no-cache"})
}
};
export const app = tenso({initRoutes});
app.start();
import {Tenso} from "tenso";
class MyAPI extends Tenso {
constructor() {
super({
auth: {
protect: ["/api/private"]
},
defaultHeaders: {
"x-api-version": "1.0.0"
}
});
this.setupRoutes();
}
setupRoutes() {
this.get("/api/health", this.healthCheck);
this.post("/api/users", this.createUser);
}
healthCheck(req, res) {
res.json({status: "healthy", timestamp: new Date().toISOString()});
}
createUser(req, res) {
// Handle user creation
res.status(201).json({message: "User created"});
}
}
const api = new MyAPI();
Routes are loaded as a module, with each HTTP method as an export, affording a very customizable API server.
You can use res
to:
res.send(body[, status, headers])
res.redirect(url)
res.error(status[, Error])
Protected routes require authorization for access and will redirect to authentication endpoints if needed.
Unprotected routes do not require authorization for access and will exit the authorization pipeline early to avoid rate limiting, CSRF tokens, & other security measures. These routes are the DMZ of your API! You must secure these endpoints with alternative methods if accepting input!
As of 17.2.0 you can have routes exit the middleware pipeline immediately by setting them in the exit
Array. This differs from unprotect
as there is no request body handling.
The /assets/*
route is reserved for the HTML browsable interface assets; please do not try to reuse this for data.
// Middleware for all requests
export const always = {
"/": (req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
}
};
// Protected routes with authentication
export const get = {
"/admin": (req, res) => {
// Only accessible after authentication
res.json({admin: true});
}
};
// Route parameters
export const get = {
"/users/:id": (req, res) => {
const userId = req.params.id;
res.json({id: userId});
}
};
Tenso decorates req
with helpers such as:
req.allow
- Allowed HTTP methodsreq.csrf
- CSRF token informationreq.ip
- Client IP addressreq.parsed
- Parsed URL informationreq.private
- Private route flagreq.body
- Request payload for PATCH
, PUT
, & POST
requestsreq.session
- Session data when using local
authenticationTenso decorates res
with helpers such as:
res.send()
- Send response with optional status and headersres.status()
- Set response statusres.json()
- Send JSON responseres.redirect()
- Send redirect responseres.error()
- Send error responseTenso is extensible and can be customized with custom parsers, renderers, & serializers.
Custom parsers can be registered with server.parser('mimetype', fn);
or directly on server.parsers
. The parameters for a parser are (arg)
.
Tenso has built-in parsers for:
application/json
application/x-www-form-urlencoded
application/jsonl
application/json-lines
text/json-lines
Custom renderers can be registered with server.renderer('mimetype', fn);
. The parameters for a renderer are (req, res, arg)
.
Tenso has built-in renderers for:
application/javascript
application/json
application/jsonl
application/json-lines
text/json-lines
application/yaml
application/xml
text/csv
text/html
Custom serializers can be registered with server.serializer('mimetype', fn);
. The parameters for a serializer are (arg, err, status = 200, stack = false)
.
Tenso has two default serializers which can be overridden:
plain
- for plain text responsescustom
- for standard response shape{
"data": null,
"error": null,
"links": [],
"status": 200
}
Responses will have a standard shape and will be UTF-8 by default. The result will be in data
. Hypermedia (pagination & links) will be in links: [{"uri": "...", "rel": "..."}, ...]
, & also in the Link
HTTP header.
Page size can be specified via the page_size
parameter, e.g. ?page_size=25
.
Sort order can be specified via the order-by
parameter which accepts [field ]asc|desc
& can be combined like an SQL ‘ORDER BY’, e.g. ?order_by=desc
or ?order_by=lastName%20asc&order_by=firstName%20asc&order_by=age%20desc
Hypermedia is a prerequisite of REST and is best described by the Richardson Maturity Model. Tenso will automatically paginate Arrays of results, or parse Entity representations for keys that imply relationships, and create the appropriate Objects in the link
Array, as well as the Link
HTTP header. Object keys that match this pattern: /_(guid|uuid|id|uri|url)$/
will be considered hypermedia links.
For example, if the key user_id
was found, it would be mapped to /users/:id
with a link rel
of related
.
Tenso will bend the rules of REST when using authentication strategies provided by passport.js, or CSRF if enabled, because they rely on a session. Session storage is in memory or Redis. You have the option of a stateless or stateful API.
Hypermedia processing of the response body can be disabled as of 10.2.0
by setting req.hypermedia = false
and/or req.hypermediaHeader
via middleware.
Tenso provides comprehensive configuration options to customize every aspect of your server. All configuration options are optional - provide only what you need to override the sensible defaults.
Option | Type | Default | Description |
---|---|---|---|
host |
string | "0.0.0.0" |
Server host address to bind to |
port |
number | 8000 |
Server port number to listen on |
title |
string | "tenso" |
Application title for branding and display |
version |
string | auto |
Framework version (auto-detected) |
silent |
boolean | false |
Suppress console output and logging |
maxListeners |
number | 25 |
Maximum number of event listeners |
Option | Type | Default | Description |
---|---|---|---|
mimeType |
string | "application/json" |
Default MIME type for responses |
charset |
string | "utf-8" |
Default character encoding for responses |
jsonIndent |
number | 0 |
JSON response indentation level (0 = minified) |
digit |
number | 3 |
Number of decimal places for numeric formatting |
renderHeaders |
boolean | true |
Include headers in rendered output responses |
time |
boolean | true |
Include timing information in response headers |
etags |
boolean | true |
Enable ETag generation for response caching |
{
defaultHeaders: {
"content-type": "application/json; charset=utf-8",
"vary": "accept, accept-encoding, accept-language, origin"
}
}
Option | Type | Default | Description |
---|---|---|---|
origins |
Array |
["*"] |
Allowed CORS origins |
corsExpose |
string | "cache-control, content-language, content-type, expires, last-modified, pragma" |
CORS exposed headers |
Option | Type | Default | Description |
---|---|---|---|
cacheSize |
number | 1000 |
Maximum number of items in memory cache |
cacheTTL |
number | 300000 |
Cache time-to-live in milliseconds (5 minutes) |
Option | Type | Default | Description |
---|---|---|---|
maxBytes |
number | 0 |
Maximum request body size in bytes (0 = unlimited) |
catchAll |
boolean | true |
Enable catch-all route handling for unmatched requests |
exit |
Array | [] |
Exit handlers to execute on server shutdown |
Option | Type | Default | Description |
---|---|---|---|
pageSize |
number | 5 |
Default pagination page size |
hypermedia.enabled |
boolean | true |
Enable hypermedia links in responses |
hypermedia.header |
boolean | true |
Include hypermedia links in response headers |
Option | Type | Default | Description |
---|---|---|---|
autoindex |
boolean | false |
Enable automatic directory indexing for static files |
webroot.root |
string | "www/" |
Document root directory for static files |
webroot.static |
string | "/assets" |
Static assets directory path |
webroot.template |
string | "template.html" |
Template file path for rendered responses |
The auth
object controls all authentication-related settings:
{
auth: {
basic: {
enabled: false, // Enable basic authentication
list: [] // Array of "username:password" strings
}
}
}
{
auth: {
bearer: {
enabled: false, // Enable bearer token authentication
tokens: [] // Array of valid bearer tokens
}
}
}
{
auth: {
jwt: {
enabled: false, // Enable JWT authentication
auth: null, // Custom JWT authentication function
audience: "", // JWT audience claim
algorithms: [ // Allowed JWT signing algorithms
"HS256", "HS384", "HS512"
],
ignoreExpiration: false, // Ignore JWT expiration
issuer: "", // JWT issuer claim
scheme: "Bearer", // JWT authentication scheme
secretOrKey: "" // JWT secret or private key
}
}
}
{
auth: {
oauth2: {
enabled: false, // Enable OAuth2 authentication
auth: null, // Custom OAuth2 authentication function
auth_url: "", // OAuth2 authorization URL
token_url: "", // OAuth2 token URL
client_id: "", // OAuth2 client ID
client_secret: "" // OAuth2 client secret
}
}
}
{
auth: {
saml: {
enabled: false, // Enable SAML authentication
auth: null // Custom SAML authentication function
}
}
}
{
auth: {
delay: 0, // Authentication delay in milliseconds
protect: [], // Routes requiring authentication (regex patterns)
unprotect: [], // Routes excluded from authentication
uri: {
login: "/auth/login", // Login endpoint URI
logout: "/auth/logout", // Logout endpoint URI
redirect: "/", // Post-authentication redirect URI
root: "/auth" // Authentication root URI
},
msg: {
login: "POST 'username' & 'password' to authenticate"
}
}
}
{
security: {
key: "x-csrf-token", // CSRF token header name
secret: "tenso", // CSRF secret key
csrf: true, // Enable CSRF protection
csp: null, // Content Security Policy header value
xframe: "SAMEORIGIN", // X-Frame-Options header value
p3p: "", // P3P privacy policy header value
hsts: null, // HTTP Strict Transport Security header
xssProtection: true, // Enable X-XSS-Protection header
nosniff: true // Enable X-Content-Type-Options: nosniff
}
}
{
session: {
cookie: {
httpOnly: true, // Set httpOnly flag on session cookies
path: "/", // Session cookie path
sameSite: true, // Enable SameSite cookie attribute
secure: "auto" // Secure cookie setting ("auto", true, false)
},
name: "tenso.sid", // Session cookie name
proxy: true, // Trust proxy for secure cookies
redis: {
host: "127.0.0.1", // Redis host address
port: 6379 // Redis port number
},
rolling: true, // Enable rolling session expiration
resave: true, // Force session save even if not modified
saveUninitialized: true, // Save uninitialized sessions
secret: "tensoABC", // Session signing secret
store: "memory" // Session store type ("memory", "redis")
}
}
{
rate: {
enabled: false, // Enable rate limiting
limit: 450, // Maximum requests per time window
message: "Too many requests", // Rate limit exceeded message
override: null, // Custom rate limit override function
reset: 900, // Rate limit reset window in seconds
status: 429 // HTTP status code for rate limit responses
}
}
{
logging: {
enabled: true, // Enable logging output
format: "%h %l %u %t \"%r\" %>s %b", // Log message format
level: "debug", // Minimum log level to output
stack: true // Include stack traces in error logs
}
}
{
prometheus: {
enabled: false, // Enable Prometheus metrics collection
metrics: {
includeMethod: true, // Include HTTP method in metrics
includePath: true, // Include request path in metrics
includeStatusCode: true, // Include status code in metrics
includeUp: true, // Include uptime metrics
buckets: [ // Histogram buckets for response times
0.001, 0.01, 0.1, 1, 2, 3, 5, 7, 10,
15, 20, 25, 30, 35, 40, 50, 70, 100, 200
],
customLabels: {} // Custom metric labels
}
}
}
{
ssl: {
cert: null, // SSL certificate file path or content
key: null, // SSL private key file path or content
pfx: null // SSL PFX file path or content
}
}
{
initRoutes: {
// Define routes to be registered on startup
get: {
"/health": (req, res) => res.json({status: "ok"})
},
post: {
"/users": userController.create
},
always: {
// Middleware that runs for all requests
"/": authMiddleware
}
}
}
Here’s a production-ready configuration example:
import {tenso} from "tenso";
const app = tenso({
// Server settings
host: "0.0.0.0",
port: process.env.PORT || 3000,
title: "My API",
// Security
auth: {
jwt: {
enabled: true,
secretOrKey: process.env.JWT_SECRET,
algorithms: ["HS256"]
},
protect: ["/api/private"]
},
security: {
csrf: true,
csp: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"]
},
hsts: {
maxAge: 31536000,
includeSubDomains: true
}
},
// Performance
rate: {
enabled: true,
limit: 1000,
reset: 3600
},
// Monitoring
prometheus: {
enabled: true
},
// Session management
session: {
store: "redis",
redis: {
host: process.env.REDIS_HOST || "localhost",
port: process.env.REDIS_PORT || 6379
},
secret: process.env.SESSION_SECRET
},
// SSL in production
ssl: {
cert: process.env.SSL_CERT_PATH,
key: process.env.SSL_KEY_PATH
}
});
const devConfig = {
logging: {level: "debug"},
rate: {enabled: false},
security: {csrf: false},
ssl: {cert: null, key: null}
};
const prodConfig = {
logging: {level: "warn"},
rate: {enabled: true, limit: 1000},
security: {csrf: true, hsts: {maxAge: 31536000}},
session: {store: "redis"},
prometheus: {enabled: true}
};
## 🔐 Authentication
The `protect` Array contains the endpoints that will require authentication. The `redirect` String is the endpoint users will be redirected to upon successfully authenticating; the default is `/`.
Sessions are used for non-`Basic` or `Bearer Token` authentication and will have `/login`, `/logout`, & custom routes. Redis is supported for session storage.
Multiple authentication strategies can be enabled at once.
Authentication attempts have a random delay to deal with "timing attacks"; always rate limit in production environments!
### Basic Auth
```javascript
{
"auth": {
"basic": {
"enabled": true,
"list": ["username:password"]
},
"protect": ["/"]
}
}
JSON Web Token (JWT) authentication is stateless and does not have an entry point. The auth(token, callback)
function must verify token.sub
and must execute callback(err, user)
.
This authentication strategy relies on out-of-band information for the secret
and other optional token attributes.
{
"auth": {
"jwt": {
"enabled": true,
"auth": function (token, cb) { /* Authentication handler to 'find' or 'create' a User */ },
"algorithms": ["HS256", "HS384", "HS512"], // Optional signing algorithms
"audience": "", // Optional, used to verify 'aud'
"issuer": "", // Optional, used to verify 'iss'
"ignoreExpiration": false, // Optional, set to true to ignore expired tokens
"scheme": "Bearer", // Optional, set to specify the Authorization scheme
"secretOrKey": ""
},
"protect": ["/private"]
}
}
OAuth2 authentication will create /auth
, /auth/oauth2
, & /auth/oauth2/callback
routes. auth(accessToken, refreshToken, profile, callback)
must execute callback(err, user)
.
{
"auth": {
"oauth2": {
"enabled": true,
"auth": function (accessToken, refreshToken, profile, callback) { /* Authentication handler */ },
"auth_url": "", // Authorization URL
"token_url": "", // Token URL
"client_id": "", // Get this from authorization server
"client_secret": "" // Get this from authorization server
},
"protect": ["/private"]
}
}
{
"auth": {
"bearer": {
"enabled": true,
"tokens": ["abc", "def", "xyz"]
},
"protect": ["/"]
}
}
SAML authentication will create /auth
, /auth/saml
, & /auth/saml/callback
routes. auth(profile, callback)
must execute callback(err, user)
.
Tenso uses passport-saml; for configuration options please visit its homepage.
{
"auth": {
"saml": {
"enabled": true
// Additional SAML configuration options go here
},
"protect": ["/private"]
}
}
Sessions can use a memory (default) or Redis store. Memory will limit your sessions to a single server instance, while Redis will allow you to share sessions across a cluster of processes or machines. To use Redis, set the store
property to “redis”.
If the session secret
is not provided, a version 4 UUID will be used.
{
"session": {
"cookie": {
"httpOnly": true,
"path": "/",
"sameSite": true,
"secure": false
},
"name": "tenso.sid",
"proxy": true,
"redis": {
"host": "127.0.0.1",
"port": 6379
},
"rolling": true,
"resave": true,
"saveUninitialized": true,
"secret": "tensoABC",
"store": "memory"
}
}
Tenso uses helmet for security headers as middleware. Please see its documentation for how to configure it; each method & argument is a key:value pair for security
.
{
"security": {
"key": "x-csrf-token",
"secret": "",
"csrf": true,
"csp": null,
"xframe": "",
"p3p": "",
"hsts": null,
"xssProtection": true,
"nosniff": true
}
}
Rate limiting is controlled by configuration and is disabled by default. Rate limiting is based on token
, session
, or ip
, depending upon authentication method.
Rate limiting can be overridden by providing an override
function that takes req
& rate
and must return a (modified) rate
.
{
"rate": {
"enabled": true,
"limit": 450, // Maximum requests allowed before reset
"reset": 900, // TTL in seconds
"status": 429, // Optional HTTP status
"message": "Too many requests", // Optional error message
"override": function (req, rate) { /* Override the default rate limiting */ }
}
}
A ‘max byte’ limit can be enforced on all routes that handle PATCH
, POST
, & PUT
requests. The default limit is 0 (unlimited).
{
"maxBytes": 5242880 // 5MB limit
}
Standard log levels are supported and are emitted to stdout
& stderr
. Stack traces can be enabled.
{
"logging": {
"level": "warn",
"enabled": true,
"stack": true
}
}
The HTML template can be overridden with a custom HTML document.
Dark mode is supported! The dark
class will be added to the body
tag if the user’s browser is in dark mode.
{
"webroot": {
"root": "/full/path/to/webroot",
"static": "/assets",
"template": "template.html"
}
}
Custom file routes can be created like this:
app.files("/folder", "/full/path/to/parent");
Create & cache an EventSource
stream to send messages to a Client. See tiny-eventsource for configuration options:
const streams = new Map();
// Route handler
"/stream": (req, res) => {
const id = req.user.userId;
if (streams.has(id) === false) {
streams.set(id, req.server.eventsource({ms: 30000}, "initialized"));
}
streams.get(id).init(req, res);
}
// Send data to Clients
streams.get(id).send({message: "Hello, World!"});
Prometheus metrics can be enabled by setting {prometheus: {enabled: true}}
. The metrics will be available at /metrics
.
{
"prometheus": {
"enabled": true,
"metrics": {
"includeMethod": true,
"includePath": true,
"includeStatusCode": true,
"includeUp": true,
"buckets": [0.001, 0.01, 0.1, 1, 2, 3, 5, 7, 10, 15, 20, 25, 30, 35, 40, 50, 70, 100, 200],
"customLabels": {}
}
}
}
Tenso includes a comprehensive benchmark suite for performance analysis and optimization. The benchmark suite tests all major framework components including HTTP handling, authentication, parsing, rendering, serialization, hypermedia generation, rate limiting, memory usage, and real-world load testing.
Tenso demonstrates excellent performance characteristics:
basic-http.js
)Tests fundamental HTTP request/response performance:
auth.js
)Tests authentication middleware performance:
parsers.js
)Tests request body parsing performance:
application/json
content parsingapplication/jsonl
line-delimited JSON parsingapplication/x-www-form-urlencoded
parsingrenderers.js
)Tests response rendering performance:
serializers.js
)Tests data serialization performance for response processing:
rate-limiting.js
)Tests rate limiting performance and accuracy:
hypermedia.js
)Tests HATEOAS link generation performance:
memory.js
)Tests memory consumption and leak detection:
load-test.js
)Real-world load testing using autocannon:
# Run all benchmarks
npm run benchmark
# Run comprehensive load test
npm run benchmark:load-test
# Run individual benchmark suites
npm run benchmark:basic # Basic HTTP performance
npm run benchmark:auth # Authentication performance
npm run benchmark:parsers # Request parsing performance
npm run benchmark:renderers # Response rendering performance
npm run benchmark:serializers # Data serialization performance
npm run benchmark:rate # Rate limiting performance
npm run benchmark:hypermedia # Hypermedia link generation
npm run benchmark:memory # Memory usage analysis
📊 Load Test Summary Report
======================================================================
| Test Category | Avg RPS | Avg Latency | P99 Latency | Error Rate |
|--------------------------------------------------------------------|
| Basic Tests | 21,899.0 | 3.1ms | 3.8ms | 0.00% |
| POST Tests | 20,522.1 | 0.7ms | 1.0ms | 0.00% |
| Format Tests | 10,634.8 | 1.7ms | 2.3ms | 0.00% |
| Rate Limit | 19,460.3 | 0.8ms | 1.3ms | 0.00% |
| Parameterized | 27,718.9 | 0.1ms | 1.0ms | 0.00% |
| Mixed Load | 25,590.4 | 1.0ms | 1.0ms | 0.00% |
| Hypermedia Enabled | 18,665.7 | 3.2ms | 4.3ms | 0.00% |
| Hypermedia Disabled| 20,694.6 | 3.2ms | 3.8ms | 0.00% |
| Stress Tests | 18,009.3 | 27.2ms | 54.8ms | 0.05% |
|--------------------------------------------------------------------|
🎯 Key Metrics:
Total tests run: 29
Total requests: 6,494,748
Total errors: 484
Overall error rate: 0.01%
Best RPS: 33,987.6
Worst RPS: 828.4
Best latency: 0.0ms
Worst latency: 60.5ms
Performance Comparison: Hypermedia Enabled vs Disabled
Test Type | With Hypermedia | Without Hypermedia | Improvement
----------------------------|-----------------|-------------------|------------
Ping Test (Simple) | 29,805.82 RPS | 33,318.55 RPS | +11.8%
JSON Response | 16,601.82 RPS | 17,472.73 RPS | +5.2%
Large JSON Response | 894.50 RPS | 902.90 RPS | +0.9%
Parameterized Route | 27,360.73 RPS | 31,084.37 RPS | +13.6%
Average Performance Impact: ~10.9% improvement when disabling hypermedia
Format | Avg RPS | Avg Latency | Throughput
----------------------------|--------------|-------------|------------
JSON | 17,141 RPS | 1.0ms | 122 MB/sec
XML | 5,948 RPS | 2.8ms | 78 MB/sec
CSV | 10,987 RPS | 1.1ms | 90 MB/sec
YAML | 8,675 RPS | 2.0ms | 60 MB/sec
Test Scenario | Connections | Duration | Avg RPS | P99 Latency
----------------------------|-------------|----------|------------|------------
High Connections | 200 | 15s | 28,334 RPS | 8ms
Very High Connections | 500 | 10s | 25,298 RPS | 121ms
JSON High Load | 100 | 15s | 16,170 RPS | 6ms
Large Response High Load | 50 | 10s | 828 RPS | 97ms
For detailed performance analysis:
# Run with garbage collection exposed for better memory benchmarking
node --expose-gc benchmarks/memory.js
# Profile CPU usage
node --prof benchmarks/basic-http.js
node --prof-process isolate-*.log > profile.txt
# Chrome DevTools integration
node --inspect benchmarks/load-test.js
# Using clinic.js (install separately)
clinic doctor -- node benchmarks/basic-http.js
For accurate benchmark results:
npm run build
“Cannot find module” errors: Ensure you’ve built the project first with npm run build
in the root directory.
High memory usage: Some benchmarks intentionally stress memory - monitor system resources.
Port conflicts: Benchmarks use random ports, but conflicts can occur with concurrent tests.
Timeout errors: Reduce iteration counts or test duration for slower systems.
For comprehensive documentation, configuration options, CI integration, and troubleshooting, see the detailed guide at benchmarks/README.md
.
Tenso is built on top of woodland, which provides the core HTTP functionality and routing.
import {tenso} from "tenso";
import assert from "node:assert";
describe("My API", () => {
let app;
beforeEach(() => {
app = tenso();
});
it("should respond to GET /", async () => {
app.get("/", (req, res) => res.send("Hello"));
const req = {method: "GET", url: "/", headers: {}};
const res = {
statusCode: 200,
headers: {},
setHeader: (k, v) => res.headers[k] = v,
end: (body) => res.body = body
};
app.route(req, res);
assert.equal(res.body, "Hello");
});
});
import {tenso} from "tenso";
const app = tenso({
auth: {
basic: {
enabled: true,
list: ["admin:password"]
},
protect: ["/api/private"]
},
defaultHeaders: {"content-type": "application/json"}
});
const users = new Map();
// Public routes
app.get("/api/health", (req, res) => {
res.json({status: "healthy"});
});
// Protected routes
app.get("/api/private/users", (req, res) => {
res.json(Array.from(users.values()));
});
app.post("/api/private/users", (req, res) => {
const user = {id: Date.now(), ...req.body};
users.set(user.id, user);
res.json(user, 201);
});
app.start();
import {tenso} from "tenso";
import {createWriteStream} from "node:fs";
import {pipeline} from "node:stream/promises";
const app = tenso({
maxBytes: 10485760 // 10MB limit
});
app.post("/upload", async (req, res) => {
try {
const filename = req.headers["x-filename"] || "upload.bin";
const writeStream = createWriteStream(`./uploads/${filename}`);
await pipeline(req, writeStream);
res.json({message: "Upload successful", filename});
} catch (error) {
res.error(500, "Upload failed");
}
});
app.start();
// Problem: Routes not protected
// Solution: Check protect array configuration
const app = tenso({
auth: {
basic: {enabled: true, list: ["user:pass"]},
protect: ["/api/private/*"] // Use wildcard for sub-routes
}
});
// Problem: CORS blocked requests
// Solution: Configure origins properly
const app = tenso({
origins: ["https://myapp.com", "http://localhost:3000"]
});
// Problem: Rate limits too strict
// Solution: Adjust rate limiting configuration
const app = tenso({
rate: {
enabled: true,
limit: 1000, // Increase limit
reset: 3600 // Longer reset window
}
});
const app = tenso({
logging: {
enabled: true,
level: "debug"
}
});
Copyright (c) 2025 Jason Mulligan
Licensed under the BSD-3-Clause license.
Built with ❤️ by Jason Mulligan