How to Build a Complete File-Sharing Application Using Filestack

Wait 5 sec.

File sharing is a common requirement across many types of applications, from internal tools and client portals to full-featured SaaS products. While the concept is straightforward, implementing it well requires handling uploads, generating shareable links, delivering files globally, and optionally supporting real-time image transformations.This guide walks through building a complete file-sharing application using Filestack for uploads, storage, CDN delivery, and image processing.What We’re BuildingA user visits your app, drops a file, and gets back a short link. Anyone with that link can preview, download, or view the file. If the file is an image, they can apply transformations like blur, crop, grayscale, and rounded corners, then copy the transformed URL, all without any server-side image processing code.What you’ll have at the end:Drag-and-drop file upload with real-time progressUnique shareable short link per fileImage previews with on-the-fly transformationsStackLayerTechnologyFrameworkNext.js (App Router)File upload & CDNFilestackDatabaseTurso (edge SQLite)ORMDrizzle ORMLanguageTypeScriptIf you don’t want to use Turso, any SQL database works. The schema is a single table.Step 1: Get Your Filestack API KeySign up at Filestack&nbsp;(there’s a generous free tier). Once you have an account, grab your API key from the dashboard. If you’re new to Filestack, the&nbsp;Quick Start guide&nbsp;walks you through the basics in a few minutes.NEXT_PUBLIC_FILESTACK_API_KEY=your_api_key_hereFilestack handlesstorage on S3&nbsp;and serves files through a&nbsp;global CDN. You don’t configure buckets or manage infrastructure. It just works.Step 2: Set Up Your DatabaseWe’re using Turso, a distributed SQLite database that runs at the edge. In development, it uses a local&nbsp;.dbfile, so there’s no cloud setup needed to get started. Follow the&nbsp;Turso quickstart&nbsp;to get set up, then add your credentials to your environment:TURSO_DATABASE_URL=libsql://your-db.turso.ioTURSO_AUTH_TOKEN=your-auth-tokenOnce that’s done, define the schema. You only need one table:// lib/db/schema.tsimport { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";export const shares = sqliteTable(&nbsp;&nbsp;"shares",&nbsp;&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;id:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; text("id").primaryKey(),&nbsp;&nbsp;&nbsp;&nbsp;code:&nbsp; &nbsp; &nbsp; &nbsp; text("code").notNull().unique(),&nbsp; &nbsp; &nbsp; // The short URL code /s/abc123&nbsp;&nbsp;&nbsp;&nbsp;handle:&nbsp; &nbsp; &nbsp; text("handle").notNull(), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // Filestack file handle&nbsp;&nbsp;&nbsp;&nbsp;url: &nbsp; &nbsp; &nbsp; &nbsp; text("url").notNull(),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // Full Filestack CDN URL&nbsp;&nbsp;&nbsp;&nbsp;filename:&nbsp; &nbsp; text("filename").notNull(),&nbsp;&nbsp;&nbsp;&nbsp;mimetype:&nbsp; &nbsp; text("mimetype").notNull(),&nbsp;&nbsp;&nbsp;&nbsp;size:&nbsp; &nbsp; &nbsp; &nbsp; integer("size").notNull(),&nbsp;&nbsp;&nbsp;&nbsp;views: &nbsp; &nbsp; &nbsp; integer("views").notNull().default(0),&nbsp;&nbsp;&nbsp;&nbsp;createdAt: &nbsp; integer("created_at").notNull(),&nbsp; &nbsp; &nbsp; // Unix ms timestamp&nbsp;&nbsp;&nbsp;&nbsp;fingerprint: text("fingerprint").notNull().default(""),&nbsp;&nbsp;},&nbsp;&nbsp;(t) => [&nbsp;&nbsp;&nbsp;&nbsp;index("shares_code_idx").on(t.code),&nbsp;&nbsp;&nbsp;&nbsp;index("shares_fingerprint_idx").on(t.fingerprint),&nbsp;&nbsp;]);The equivalent SQL, if you prefer to see it plainly:CREATE TABLE shares (&nbsp;&nbsp;id&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; TEXT PRIMARY KEY,&nbsp;&nbsp;code&nbsp; &nbsp; &nbsp; &nbsp; TEXT NOT NULL UNIQUE,&nbsp;&nbsp;handle&nbsp; &nbsp; &nbsp; TEXT NOT NULL,&nbsp;&nbsp;url &nbsp; &nbsp; &nbsp; &nbsp; TEXT NOT NULL,&nbsp;&nbsp;filename&nbsp; &nbsp; TEXT NOT NULL,&nbsp;&nbsp;mimetype&nbsp; &nbsp; TEXT NOT NULL,&nbsp;&nbsp;size&nbsp; &nbsp; &nbsp; &nbsp; INTEGER NOT NULL,&nbsp;&nbsp;views &nbsp; &nbsp; &nbsp; INTEGER NOT NULL DEFAULT 0,&nbsp;&nbsp;created_at&nbsp; INTEGER NOT NULL,&nbsp;&nbsp;fingerprint TEXT NOT NULL DEFAULT '');CREATE INDEX shares_code_idx&nbsp; &nbsp; &nbsp; &nbsp; ON shares(code);CREATE INDEX shares_fingerprint_idx ON shares(fingerprint);Run&nbsp;npx drizzle-kit push&nbsp;to create the table on Turso.Step 3: Upload Files Directly to FilestackYou can use the&nbsp;Filestack File Picker&nbsp;for a drop-in upload UI, or use one of the&nbsp;official SDKs&nbsp;(JavaScript, React, Angular, Python, and more). For this project, we went with a plain XHR call to the&nbsp;Store API&nbsp;to get native progress events:async function uploadToFilestack(&nbsp;&nbsp;file: File,&nbsp;&nbsp;apiKey: string,&nbsp;&nbsp;onProgress: (percent: number) => void,) {&nbsp;&nbsp;return new Promise(&nbsp;&nbsp;&nbsp;&nbsp;(resolve, reject) => {&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;const xhr = new XMLHttpRequest();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;const params = new URLSearchParams({ key: apiKey, filename: file.name });&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;xhr.open("POST", `https://www.filestackapi.com/api/store/S3?${params}`);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;xhr.upload.onprogress = (evt) => {&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (evt.lengthComputable) {&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;onProgress(Math.round((evt.loaded / evt.total) * 100));&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;};&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;xhr.onload = () => {&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (xhr.status >= 200 && xhr.status < 300) {&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;resolve(JSON.parse(xhr.responseText));&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} else {&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;reject(new Error(`Upload failed: ${xhr.status}`));&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;};&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;xhr.onerror = () => reject(new Error("Network error"));&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;xhr.send(file);&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;);}Filestack responds with a URL like this:https://cdn.filestackcontent.com/AbCdEfGh1234567890abThat last segment is the handle. Save it. You’ll use it to build transformation URLs later.function extractHandle(url: string): string {&nbsp;&nbsp;return url.replace("https://cdn.filestackcontent.com/", "").split("?")[0];}Step 4: Save the Share to Your DatabaseAfter the upload succeeds, call your own API route to save the metadata and generate a short shareable code:// app/api/share/route.tsimport { NextResponse } from "next/server";import { nanoid } from "nanoid";import { db, schema } from "@/lib/db";export async function POST(req: Request) {&nbsp;&nbsp;const { handle, url, filename, mimetype, size } = await req.json();&nbsp;&nbsp;if (!/^https:\/\/cdn\.filestackcontent\.com\//.test(url)) {&nbsp;&nbsp;&nbsp;&nbsp;return NextResponse.json({ error: "Invalid URL" }, { status: 400 });&nbsp;&nbsp;}&nbsp;&nbsp;const id = nanoid();&nbsp;&nbsp;const code = nanoid(10);&nbsp;&nbsp;await db.insert(schema.shares).values({&nbsp;&nbsp;&nbsp;&nbsp;id, code, handle, url, filename, mimetype, size,&nbsp;&nbsp;&nbsp;&nbsp;createdAt: Date.now(),&nbsp;&nbsp;});&nbsp;&nbsp;return NextResponse.json({ code });}The client then redirects the user to&nbsp;/s/{code},&nbsp;where the share page lives.Step 5: The Share PageWhen someone opens&nbsp;/s/abc123, look up the record and render the file preview:// app/s/[code]/page.tsxasync function getShare(code: string) {&nbsp;&nbsp;const rows = await db&nbsp;&nbsp;&nbsp;&nbsp;.select()&nbsp;&nbsp;&nbsp;&nbsp;.from(schema.shares)&nbsp;&nbsp;&nbsp;&nbsp;.where(eq(schema.shares.code, code))&nbsp;&nbsp;&nbsp;&nbsp;.limit(1);&nbsp;&nbsp;return rows[0];}Bump the view count while you're at it:async function bumpViews(id: string) {&nbsp;&nbsp;await db&nbsp;&nbsp;&nbsp;&nbsp;.update(schema.shares)&nbsp;&nbsp;&nbsp;&nbsp;.set({ views: sql`${schema.shares.views} + 1` })&nbsp;&nbsp;&nbsp;&nbsp;.where(eq(schema.shares.id, id));}For the preview itself, check the MIME type and render accordingly. Images get an&nbsp;&nbsp;tag, PDFs can use&nbsp;Filestack’s Document Viewer, and videos get a&nbsp;&nbsp;element. Filestack’s CDN handles delivery for all of them.Step 6: On-The-Fly Image TransformationsOnce a file is uploaded, you can transform it by changing the URL. No server-side image processing, no extra libraries.Filestack’s Processing API&nbsp;supports resize, crop, rotate, filters, and more. Transformations are chained as URL segments:TransformationURLResize to 300pxhttps://cdn.filestackcontent.com/resize=width:300/HANDLESquare crophttps://cdn.filestackcontent.com/resize=width:300,height:300,fit:crop/HANDLEGrayscalehttps://cdn.filestackcontent.com/monochrome/HANDLEBlurhttps://cdn.filestackcontent.com/blur=amount:8/HANDLESepiahttps://cdn.filestackcontent.com/sepia=tone:80/HANDLERounded cornershttps://cdn.filestackcontent.com/rounded_corners=radius:30/HANDLEPolaroid effecthttps://cdn.filestackcontent.com/polaroid/HANDLERotate 90°https://cdn.filestackcontent.com/rotate=deg:90/HANDLEYou can chain them. Here’s a grayscale, rounded-corners thumbnail in a single URL:https://cdn.filestackcontent.com/resize=width:300/monochrome/rounded_corners=radius:30/HANDLEFilestack also offers a&nbsp;Transformations UI&nbsp;if you want to give users a visual editor instead of building your own. And for video/audio, there’s a dedicated&nbsp;Video and Audio Processing API.What Else Can You Build With This?Once Filestack is hooked up, the same pattern can power a lot more:ProductHowUser profile avatarsUpload, crop, resize, then save the handle to the user recordE-commerce product imagesUpload once, generate any size variant via the URLClient document portalUpload files and serve them from short, access-controlled linksVideo hosting (MVP)Upload MP4, serve directly via Filestack CDN with a&nbsp;&nbsp;tagImage-heavy blog CMSEditors upload once, and the frontend requests the right size per breakpointDeploy ChecklistBefore going live:NEXT_PUBLIC_FILESTACK_API_KEY&nbsp;set in your hosting environmentTURSO_DATABASE_URL&nbsp;and&nbsp;TURSO_AUTH_TOKEN&nbsp;are set for productionnpx drizzle-kit push&nbsp;run against your production Turso databaseFilestack Security Policies&nbsp;are configured to restrict file types and sizes at the API levelFurther ReadingIf you want to go deeper, here are some good next steps:TopicLinkGetting started with FilestackQuick StartHow Filestack works under the hoodHow Filestack WorksFile Picker (drop-in upload UI)File PickersGenerate a picker with no codePicker Code GeneratorAll image and file transformationsProcessing API ReferenceVideo and audio processingVideo & Audio APIAI features (tagging, OCR, virus scan)IntelligenceAutomate file workflowsWorkflowsSDKs (JS, React, Python, Go, etc.)Framework SDKsSecurity and access controlSecurity PoliciesStep-by-step tutorialsTutorialsBlog and more use casesFilestack BlogFinal ThoughtsThe thing that makes Filestack worth using isn’t just the upload API. Once a file is uploaded, you get CDN delivery and the Processing API included. You write zero image processing code, you manage zero storage infrastructure, and you can still deliver a rich experience from a single file handle.Try the&nbsp;live demo&nbsp;at fireshare-swart.vercel.app or grab the&nbsp;source code on GitHub.Start there, strip it down to your use case, and you’ll have something production-ready faster than you’d expect.