Express
Overview
Section titled “Overview”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.
Installation
Section titled “Installation”npm install @uploadista/server @uploadista/adapters-express expresspnpm add @uploadista/server @uploadista/adapters-express expressyarn add @uploadista/server @uploadista/adapters-express expressQuick Start
Section titled “Quick Start”First, define your flows:
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 routesapp.all("/uploadista/api/*splat", (request, response, next) => { uploadistaServer.handler({ request, response, next });});
app.listen(3000, () => { console.log("Uploadista server running on http://localhost:3000");});Configuration Options
Section titled “Configuration Options”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)});Complete Setup Example
Section titled “Complete Setup Example”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 Redisconst redisClient = createClient({ url: process.env.REDIS_URL || "redis://localhost:6379",});const redisSubscriberClient = redisClient.duplicate();await redisClient.connect();await redisSubscriberClient.connect();
// Create Express appconst app = express();app.use(express.json({ limit: "50mb" }));app.use(express.urlencoded({ extended: true }));
// CORS middlewareapp.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 authenticationconst 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 routesapp.all("/uploadista/api/*splat", (request, response, next) => { uploadistaServer.handler({ request, response, next });});
// Health checkapp.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 shutdownprocess.on("SIGTERM", async () => { console.log("SIGTERM received, closing server..."); await uploadistaServer.dispose(); await redisClient.quit(); process.exit(0);});const express = require("express");const { createUploadistaServer } = require("@uploadista/server");const { expressAdapter } = require("@uploadista/adapters-express");const { fileStore } = require("@uploadista/data-store-filesystem");const { fileKvStore } = require("@uploadista/kv-store-filesystem");
const app = express();app.use(express.json({ limit: "50mb" }));
createUploadistaServer({ dataStore: fileStore({ directory: "./uploads" }), kvStore: fileKvStore({ directory: "./uploads" }), flows, adapter: expressAdapter(),}).then((uploadistaServer) => { app.all("/uploadista/api/*splat", (request, response, next) => { uploadistaServer.handler({ request, response, next }); });
app.listen(3000, () => { console.log("Server running on port 3000"); });});API Routes
Section titled “API Routes”The server automatically creates these routes:
POST /uploadista/api/upload Create uploadGET /uploadista/api/upload/:uploadId Get upload statusPATCH /uploadista/api/upload/:uploadId Upload chunkDELETE /uploadista/api/upload/:uploadId Delete upload
POST /uploadista/api/flow/:flowId/:storageId Execute flowGET /uploadista/api/jobs/:jobId/status Get job statusPATCH /uploadista/api/jobs/:jobId/resume/:nodeId Resume paused flow
GET /uploadista/api/health Health checkGET /uploadista/api/ready Readiness checkAuthentication
Section titled “Authentication”JWT Authentication
Section titled “JWT Authentication”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; } },})API Key Authentication
Section titled “API Key Authentication”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, }; },})Middleware
Section titled “Middleware”Rate Limiting
Section titled “Rate Limiting”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);Request Logging
Section titled “Request Logging”import morgan from "morgan";
app.use(morgan("combined"));Error Handling
Section titled “Error Handling”app.use((err, req, res, next) => { console.error("Error:", err); res.status(500).json({ error: "Internal server error", message: err.message, });});Deployment
Section titled “Deployment”Docker
Section titled “Docker”FROM node:20-alpine
WORKDIR /app
COPY package*.json ./RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "dist/server.js"]Docker Compose
Section titled “Docker Compose”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"Heroku
Section titled “Heroku”# Create Heroku appheroku create my-uploadista-server
# Add Redis addonheroku addons:create heroku-redis:hobby-dev
# Set environment variablesheroku config:set S3_BUCKET=my-uploadsheroku config:set AWS_REGION=us-east-1heroku config:set AWS_ACCESS_KEY_ID=xxxheroku config:set AWS_SECRET_ACCESS_KEY=xxx
# Deploygit push heroku mainBest Practices
Section titled “Best Practices”Graceful Shutdown
Section titled “Graceful Shutdown”process.on("SIGTERM", async () => { console.log("SIGTERM received"); await uploadistaServer.dispose(); // Flushes pending traces await redisClient.quit(); process.exit(0);});Health Checks
Section titled “Health Checks”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 checksapp.get("/health", async (req, res) => { try { await redisClient.ping(); res.json({ status: "healthy" }); } catch (error) { res.status(503).json({ status: "unhealthy", error: error.message }); }});Related Concepts
Section titled “Related Concepts”- Authentication - Authentication and permissions
- Uploadista Server - Server configuration
- Data Stores - File storage backends
- KV Stores - Metadata storage
- Event System - Real-time events