LinkLibrarian is a full-stack web app for registering users, logging in, and saving, editing, filtering, and deleting personal links.
Example comment of change.
- Frontend: React + Vite
- Backend: Node.js + Express
- Database: MySQL
- Sessions: express-session + MySQL session store
- Hosting: Railway
project-root/
backend/
frontend/
backend/contains the Express server, session handling, and MySQL connection.frontend/contains the Vite + React client.
- User registration and login
- Session-based authentication
- Create, read, update, and delete links
- Tag filtering
- Local development support
- Production deployment on Railway
Open a terminal in the backend/ folder and run:
npm install
npm startThe backend runs on:
http://localhost:3001
You can verify it with:
http://localhost:3001/http://localhost:3001/api/test-db
Open a second terminal in the frontend/ folder and run:
npm install
npm run devThe frontend runs on:
http://localhost:5173
This project uses Vite environment variables for switching between local and production API URLs. Vite exposes only variables prefixed with VITE_ to client-side code.
VITE_API_BASE_URL=http://localhost:3001This is used for local development.
VITE_API_BASE_URL=https://linklibrarian-production.up.railway.appThis is used for production builds.
In frontend/src/App.jsx, the API base URL is read like this:
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001';This app is deployed on Railway as three services:
- MySQL database
- Express backend
- Vite/React frontend
https://linklibrarian-frontend-production.up.railway.app
https://linklibrarian-production.up.railway.app
The frontend service is configured with:
- Root Directory:
/frontend - Public domain target port:
8080
Railway detects the frontend as a Vite static site, builds it into dist, and serves it with Caddy.
The backend allows both local development and the Railway-hosted frontend by using an allowlist in server.js.
Example:
const allowedOrigins = [
'http://localhost:5173',
'https://linklibrarian-frontend-production.up.railway.app'
];
app.use(cors({
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));Important:
- Do not add a trailing slash to allowed origins.
http://localhost:5173works, buthttp://localhost:5173/causes CORS errors.
In the Railway frontend service, set:
VITE_API_BASE_URL=https://linklibrarian-production.up.railway.app
Because Vite frontend variables are baked into the build, changing this value requires a rebuild/redeploy.
.env.localshould stay local and should not usually be committed.VITE_variables are not secret; they are visible in the frontend bundle.- Database passwords, session secrets, and backend credentials must stay in backend or Railway service variables, not frontend env files.
- Start local backend
- Start local frontend
- Open
http://localhost:5173 - Confirm changes affect local MySQL only
- Open
https://linklibrarian-frontend-production.up.railway.app - Register/login successfully
- Confirm CRUD actions affect Railway MySQL
Common causes:
- backend is not running locally
- frontend env variable points to the wrong API URL
- Railway frontend variable was changed but the app was not rebuilt
- CORS origin does not exactly match the frontend URL
Check:
- exact frontend origin
- no trailing slash in CORS allowlist
credentials: trueis enabled- deployed frontend URL is included in
allowedOrigins
Make sure both are running:
- backend at
http://localhost:3001 - frontend at
http://localhost:5173