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â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"# 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