A full-stack file management web app built as the File Uploader Assignment from The Odin Project.
This is my most complex project yet: from concept and UI design to backend architecture, database modeling, authentication/authorization, cloud file integration, and public sharing workflows.
I made this project 95% without using AI.
I designed and implemented the major parts myself end-to-end:
- product design and feature planning
- UI structure and styling
- backend architecture and route/controller/service structure
- database schema and migration workflow
- API integration with Cloudinary
- authentication and authorization system
- protected/private and public sharing flows
Name: ShareFY (File Uploader)
Core idea:
- Authenticated users create folders
- Upload files inside folders
- Rename/delete files and folders
- Download private files securely
- Generate expiring public share links for files or whole folders
This project goes beyond basic CRUD. It combines:
- session-based auth with Passport
- ownership-based authorization
- cloud storage + signed private downloads
- relational data modeling with Prisma
- expiring share links with scheduled cleanup
- validation and error boundaries across the request lifecycle
- Runtime: Node.js
- Language: TypeScript
- Framework: Express 5
- Templating: EJS
- Database: PostgreSQL
- ORM: Prisma
- Auth: Passport (local strategy) + express-session
- Session Store: Prisma session store (
@quixo3/prisma-session-store) - File Upload Parser: Multer
- Cloud Storage: Cloudinary API
- Validation: express-validator
- Scheduler: node-cron
- Styling/Frontend: Vanilla JS + CSS + EJS views
The app follows a clean layered structure:
- Routes define URL contracts
- Controllers handle request/response orchestration
- Services encapsulate reusable database/business logic
- Utils encapsulate reusable low-level I/O helpers (download streaming)
- Validators enforce input constraints
- Middleware handles auth guarding, user context, and global errors
- Prisma layer handles typed persistence
- Cloudinary integration handles external file hosting and signed file delivery
- Request enters Express app.
- Session middleware restores session from Prisma store.
- Passport deserializes the user (if logged in).
userMiddlewareinjectscurrentUserintores.localsfor EJS.- Route-level middleware (
isAuth) guards protected resources. - Validators run and normalize input.
- Controller executes business flow (DB + Cloudinary).
- Result is rendered in EJS view or returned as file download.
- Errors are centralized through global
errorMiddleware.
src/
config/
passport.ts # Local strategy + serialize/deserialize
controllers/
auth.controller.ts
file.controller.ts
folder.controller.ts
home.controller.ts
publicshare.controller.ts
lib/
multer.ts # Upload destination + 10MB upload cap
prisma.ts # Prisma + PostgreSQL adapter
middleware/
auth.middleware.ts # Route protection via req.isAuthenticated()
error.middleware.ts # Central error rendering
user.middleware.ts # Expose currentUser to templates
routes/
auth.router.ts
file.router.ts
folder.router.ts
home.router.ts
publicshare.router.ts
services/
file.service.ts # Upload workflow + quota check + Cloudinary metadata persistence
folder.service.ts # Folder queries + user metrics
utils/
file.stream.ts # Shared Cloudinary-to-local streaming + res.download delivery
validators/
file.validator.ts
folder.validator.ts
user.validator.ts
index.ts # App bootstrap + middleware + routes + cron
prisma/
schema.prisma # Data model
migrations/ # Migration history
views/ # EJS templates
public/ # Static CSS/JS/icons
uploads/ # Temporary local upload buffer (pre-cloud)
downloads/ # Temporary local download buffer
Prisma models represent users, folders, files, share links, and session persistence.
- User owns many folders and files.
- Folder belongs to one user and has many files.
- File belongs to one user and one folder.
- FolderShare is a public share token with expiry for a folder.
- FileShare is a public share token with expiry for a single file.
- Session stores express-session data in PostgreSQL.
- One active share record per user-folder pair:
@@unique([folderID, userID]) - One active share record per user-file pair:
@@unique([fileID, userID])
This prevents duplicate share rows for the same resource-owner pair.
- Local auth strategy with
email + password - Passwords are hashed with bcrypt before storage
- Login uses Passport local strategy
- Sessions are persisted in PostgreSQL through Prisma session store
Authorization is ownership-first:
- Protected routers (
/folder,/file) are wrapped inisAuth - Resource operations always include
userID/authorIDchecks in Prisma queries - Unauthorized access returns error pages (401/403/404 based on context)
- Multer accepts the file (
uploadedFile) into localuploads/. - Request is validated (
fileUploadRules). FileService.uploadFile()aggregates user storage from DB.- If new upload exceeds quota, request is denied.
- Service uploads to Cloudinary (
type: private). - Service persists metadata in Prisma (
public_id,resource_type,version, size, ext, etc.). - Controller always deletes local temporary upload file in
finallycleanup.
- Per file limit (Multer): 10MB
- Per account logical quota: 50MB (in total)
- Verify file ownership in DB.
- Delegate stream/download work to
FileStreamUtil.downloadFile(). - Utility generates signed Cloudinary URL for private asset download.
- Utility streams remote file into local
downloads/using a unique temporary filename (collision-safe). - Utility serves via
res.download()using the clean user-facing filename (name.ext). - Utility deletes temporary local file after response.
The same download utility is used by:
- private owner downloads (
/file/:id/download) - public shared-file downloads (
/publicshare/file/:shareID/download) - shared-folder file downloads (
/publicshare/folder/:shareID/file/:fileID/download)
The app supports two public link types:
- Folder share: public access to folder listing + file downloads within that folder
- File share: public access to one file + download
- User chooses expiry duration (1-60 days)
- Controller computes
expiresAt - Share record is created in DB
- Public routes validate share ID existence and expiry (
expiresAt > now) - Shared-folder file download additionally checks that file belongs to shared folder
A cron job runs daily at UTC midnight and removes expired records from:
FolderShareFileShare
This keeps the public link surface short-lived and controlled.
Validation is handled with express-validator and follows route context:
- User validation
- Non-empty first/last name
- Email format and uniqueness
- Password confirmation match
- Folder validation
- Folder name required and max length
- Share duration integer constraints
- File validation
- Optional upload rename constraints
- Edit filename and folder ID checks
- Share duration integer constraints
When validation fails, forms re-render with errors and previous input where relevant.
GET /-> Landing pageGET /auth/sign-up-> Sign-up formPOST /auth/sign-up-> Register userGET /auth/log-in-> Login formPOST /auth/log-in-> Authenticate userGET /auth/log-out-> LogoutGET /publicshare/folder/:shareID-> View shared folderGET /publicshare/folder/:shareID/file/:fileID/download-> Download file from shared folderGET /publicshare/file/:shareID-> View shared fileGET /publicshare/file/:shareID/download-> Download shared file
GET /folder/all-> Dashboard + folder list + metricsPOST /folder/new-> Create folderPOST /folder/:id/edit-> Rename folderPOST /folder/:id/delete-> Delete folderPOST /folder/:id/file-> Upload file to folderPOST /folder/:id/share-> Create folder share linkGET /folder/:id/share/delete-> Revoke folder share linkGET /folder/:id-> Open folder page
POST /file/:id/edit-> Rename filePOST /file/:id/delete-> Delete fileGET /file/:id/download-> Private owner downloadPOST /file/:id/share-> Create file share linkGET /file/:id/share/delete-> Revoke file share link
The frontend is intentionally server-rendered first with EJS, then enhanced with a strong client-side interaction layer in public/script.js.
- EJS renders pages on the server with real user/session context (
currentUser, folder/file data, metrics, errors). - Shared partials (like header and error blocks) keep UI consistent across pages.
- Views are purpose-driven:
landing.ejsfor product/marketing entrysign-up.ejsandlog-in.ejsfor auth flowfolders.ejsfor dashboard metrics + folder managementfolder.ejsfor file operations inside a foldersharedfolder.ejsandsharedfile.ejsfor public share access
public/script.js acts as the UI behavior engine and controls much of the interactive experience:
- Header behavior on scroll (
.scrolledclass toggle) - Dialog lifecycle management for folder/file actions:
- create
- edit
- delete
- share
- Dynamic form target rewriting (injecting correct resource IDs into form actions)
- Reusable custom dropdown system (
CustomDropDownMenu+ActionItem) for per-card action menusCustomDropDownMenuis from my own npm package that I had released months before this project- I customized its UI/UX behavior and styling specifically for ShareFY
- Share-link UX orchestration:
- switching between create mode and already-shared mode
- copy-to-clipboard for public URLs
- expiry countdown display in days
- delete-link action wiring
- Client-side guardrails before upload:
- max file size check (10MB)
- max filename length check
- Client-side date localization:
- UTC timestamps rendered by server are formatted into user-friendly local dates in-browser
- EJS embeds resource metadata into
data-*attributes on menu buttons (data-folder-id,data-file-share-id, etc.). script.jsreads those attributes and builds context-aware UI actions at runtime.- This keeps the server responsible for trusted data and the client responsible for interaction ergonomics.
- Multi-page app architecture (MPA) over SPA complexity.
- Fast first render from server templates.
- Progressive enhancement on top of semantic HTML dialogs/forms.
- No heavy frontend framework dependency; behavior remains transparent and maintainable.
Create .env based on .env.example.
Required values:
PORT=3500
DATABASE_URL="postgresql://user:password@localhost:5432/mydatabase?schema=public"
SESSION_SECRET="replace_with_a_secure_secret"
CLOUDINARY_URL="cloudinary://api_key:api_secret@cloud_name"- Install dependencies:
npm install-
Configure environment variables in
.env. -
Run Prisma migrations (development):
npx prisma migrate dev- Generate Prisma client (optional,
migrate devusually generates automatically):
npx prisma generate- Start development server:
npm run dev- Open the app:
http://localhost:3500
npm run build
npm startnpm run dev-> run app in watch mode withtsxnpm run build-> compile TypeScript todist/npm start-> run compiled server fromdist/index.js
This project was created as the File Uploader project from The Odin Project and intentionally pushes beyond the minimum assignment baseline:
- stronger data modeling
- robust auth and protected route layering
- cloud-based private/public file delivery patterns
- expiring share-link lifecycle management
- production-minded cleanup and error handling patterns
Version: 1.0
This is currently the most complex project in my Odin journey, and it represents a major step in full-stack system design, backend architecture, and real-world integration work.
