Skip to content

Express

The Express adapter integrates Uploadista with Express.js applications, providing HTTP endpoints and WebSocket support for file uploads and flow execution. Ideal for traditional Node.js deployments on Heroku, AWS, DigitalOcean, and VPS hosting.

npm install @uploadista/server @uploadista/adapters-express express

First, define your flows:

flows.ts
import {
createFlow,
createInputNode,
createOutputNode,
createOptimizeNode,
} from "@uploadista/flow";
export const optimizeFlow = createFlow({
flowId: "optimize-image",
name: "Optimize Image",
nodes: {
input: createInputNode("input"),
optimize: createOptimizeNode("optimize", { format: "webp", quality: 80 }),
output: createOutputNode("output"),
},
edges: [
{ source: "input", target: "optimize" },
{ source: "optimize", target: "output" },
],
});
export const flows = {
"optimize-image": optimizeFlow,
};

Then set up the server:

import express from "express";
import { createUploadistaServer } from "@uploadista/server";
import { expressAdapter } from "@uploadista/adapters-express";
import { fileStore } from "@uploadista/data-store-filesystem";
import { fileKvStore } from "@uploadista/kv-store-filesystem";
import { flows } from "./flows";
const app = express();
app.use(express.json({ limit: "50mb" }));
const uploadistaServer = await createUploadistaServer({
dataStore: fileStore({ directory: "./uploads" }),
kvStore: fileKvStore({ directory: "./uploads" }),
flows,
adapter: expressAdapter(),
});
// Mount HTTP routes
app.all("/uploadista/api/*splat", (request, response, next) => {
uploadistaServer.handler({ request, response, next });
});
app.listen(3000, () => {
console.log("Uploadista server running on http://localhost:3000");
});
import { imagePlugin } from "@uploadista/flow-images-sharp";
import { redisEventBroadcaster } from "@uploadista/event-broadcaster-redis";
const uploadistaServer = await createUploadistaServer({
// Required
dataStore: s3Store({ /* config */ }), // File storage backend
kvStore: redisKvStore({ /* config */ }), // Metadata storage
flows, // Flow definitions object
adapter: expressAdapter({
authMiddleware: async (ctx) => ({ clientId: "user-123" }), // Optional
}),
// Optional
plugins: [imagePlugin()], // Processing plugins
eventBroadcaster: redisEventBroadcaster({ /* config */ }), // Multi-instance sync
withTracing: true, // OpenTelemetry tracing
baseUrl: "uploadista", // API base path (default)
});
import express from "express";
import { createClient } from "redis";
import { createUploadistaServer } from "@uploadista/server";
import { expressAdapter } from "@uploadista/adapters-express";
import { s3Store } from "@uploadista/data-store-s3";
import { redisKvStore } from "@uploadista/kv-store-redis";
import { redisEventBroadcaster } from "@uploadista/event-broadcaster-redis";
import { imagePlugin } from "@uploadista/flow-images-sharp";
import { flows } from "./flows";
// Initialize Redis
const redisClient = createClient({
url: process.env.REDIS_URL || "redis://localhost:6379",
});
const redisSubscriberClient = redisClient.duplicate();
await redisClient.connect();
await redisSubscriberClient.connect();
// Create Express app
const app = express();
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ extended: true }));
// CORS middleware
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS");
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (req.method === "OPTIONS") return res.sendStatus(200);
next();
});
// Create Uploadista server with authentication
const uploadistaServer = await createUploadistaServer({
dataStore: s3Store({
deliveryUrl: process.env.CDN_URL!,
s3ClientConfig: {
bucket: process.env.S3_BUCKET!,
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
},
}),
kvStore: redisKvStore({ redis: redisClient }),
eventBroadcaster: redisEventBroadcaster({
redis: redisClient,
subscriberRedis: redisSubscriberClient,
}),
flows,
plugins: [imagePlugin()],
adapter: expressAdapter({
// JWT authentication
authMiddleware: async (ctx) => {
const authHeader = ctx.request.headers.authorization;
if (!authHeader) return null;
const token = authHeader.split(" ")[1];
if (!token) return null;
try {
const payload = await verifyJWT(token, process.env.JWT_SECRET!);
return {
clientId: payload.sub,
permissions: payload.permissions || [],
};
} catch (error) {
console.error("Auth error:", error);
return null;
}
},
}),
});
// Mount Uploadista routes
app.all("/uploadista/api/*splat", (request, response, next) => {
uploadistaServer.handler({ request, response, next });
});
// Health check
app.get("/health", (req, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`🚀 Uploadista server running on port ${PORT}`);
});
// Graceful shutdown
process.on("SIGTERM", async () => {
console.log("SIGTERM received, closing server...");
await uploadistaServer.dispose();
await redisClient.quit();
process.exit(0);
});

The server automatically creates these routes:

POST /uploadista/api/upload Create upload
GET /uploadista/api/upload/:uploadId Get upload status
PATCH /uploadista/api/upload/:uploadId Upload chunk
DELETE /uploadista/api/upload/:uploadId Delete upload
POST /uploadista/api/flow/:flowId/:storageId Execute flow
GET /uploadista/api/jobs/:jobId/status Get job status
PATCH /uploadista/api/jobs/:jobId/resume/:nodeId Resume paused flow
GET /uploadista/api/health Health check
GET /uploadista/api/ready Readiness check
adapter: expressAdapter({
authMiddleware: async (ctx) => {
const token = ctx.request.headers.authorization?.split(" ")[1];
if (!token) return null;
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
return {
clientId: payload.sub,
permissions: payload.permissions || [],
};
} catch {
return null;
}
},
})
adapter: expressAdapter({
authMiddleware: async (ctx) => {
const apiKey = ctx.request.headers["x-api-key"];
if (!apiKey) return null;
const client = await db.clients.findOne({ apiKey });
if (!client) return null;
return {
clientId: client.id,
permissions: client.permissions,
};
},
})
import rateLimit from "express-rate-limit";
const uploadLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: "Too many upload requests, please try again later",
});
app.use("/uploadista/api/upload", uploadLimiter);
import morgan from "morgan";
app.use(morgan("combined"));
app.use((err, req, res, next) => {
console.error("Error:", err);
res.status(500).json({
error: "Internal server error",
message: err.message,
});
});
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "dist/server.js"]
version: '3.8'
services:
uploadista:
build: .
ports:
- "3000:3000"
environment:
- REDIS_URL=redis://redis:6379
- S3_BUCKET=my-uploads
- AWS_REGION=us-east-1
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
depends_on:
- redis
redis:
image: redis:7-alpine
ports:
- "6379:6379"
Terminal window
# Create Heroku app
heroku create my-uploadista-server
# Add Redis addon
heroku addons:create heroku-redis:hobby-dev
# Set environment variables
heroku config:set S3_BUCKET=my-uploads
heroku config:set AWS_REGION=us-east-1
heroku config:set AWS_ACCESS_KEY_ID=xxx
heroku config:set AWS_SECRET_ACCESS_KEY=xxx
# Deploy
git push heroku main
process.on("SIGTERM", async () => {
console.log("SIGTERM received");
await uploadistaServer.dispose(); // Flushes pending traces
await redisClient.quit();
process.exit(0);
});

Uploadista provides built-in health endpoints:

// Built-in endpoints (no additional setup needed)
// GET /uploadista/api/health - Liveness probe
// GET /uploadista/api/ready - Readiness probe
// Or add custom health checks
app.get("/health", async (req, res) => {
try {
await redisClient.ping();
res.json({ status: "healthy" });
} catch (error) {
res.status(503).json({ status: "unhealthy", error: error.message });
}
});