I am a second year B.Tech Computer Science student at NIT Patna.
Two weeks ago I had never built or deployed a real backend project.
Today I have a live API running on the internet that anyone can use.
This is the story of how I built it, what I learned, and the specific
mistakes I made along the way.
What I Built
A URL Shortener API. You give it a long URL. It gives you a short code.
When someone visits the short link, they get redirected to the original URL.
Sounds simple. Building it properly was not.
Live URL: https://urlshortener-api-production.up.railway.app/health
GitHub: https://urlshortener-api-production.up.railway.app
The Tech Stack
- Node.js and Express for the server
- PostgreSQL for the database
- JWT for authentication
- bcrypt for password hashing
- nanoid for generating short codes
- Docker for containerization
- Railway for deployment
The Architecture — Why MVC Matters
The first version of my project had everything in one file. Routes,
database queries, validation, business logic — all 200 lines in app.js.
It worked. But it was unreadable.
I refactored into MVC architecture. Model View Controller.
Here is what that actually means in practice:
Models handle all database queries. Nothing else.
They know nothing about HTTP requests or responses.
Controllers handle the request, call models for data,
and send the response. They never write SQL directly.
Routes just map URLs to controller functions.
A route file should be readable in 30 seconds.
Middleware handles cross-cutting concerns.
JWT verification, input validation, rate limiting.
My folder structure ended up looking like this:
src/
├── config/
│ └── db.js
├── models/
│ ├── userModel.js
│ └── urlModel.js
├── controllers/
│ ├── authController.js
│ └── urlController.js
├── routes/
│ ├── authRoutes.js
│ └── urlRoutes.js
├── middleware/
│ ├── auth.js
│ ├── validate.js
│ └── rateLimiter.js
└── utils/
└── generateCode.js
When something breaks now, I know exactly which file to open.
That is what clean architecture gives you.
How JWT Authentication Works in This Project
Most tutorials show you how to use JWT. Very few explain why
each decision was made.
Here is my implementation and the reasoning behind it:
Why two tokens instead of one?
Access tokens expire in 15 minutes. Refresh tokens expire in 7 days.
If an access token is stolen, the attacker only has 15 minutes.
Short enough to limit damage. Long enough that users are not
logging in every 15 minutes.
Refresh tokens are stored in the database. When a user logs out,
the refresh token is deleted. The next time someone tries to use
that refresh token, it fails. That is real logout functionality.
With a single long-lived token, you cannot log someone out.
The token works until it expires regardless.
Why bcrypt for passwords?
bcrypt is intentionally slow. It takes about 300 milliseconds
to hash one password.
That sounds like a problem. It is actually the feature.
SHA-256 computes billions of hashes per second. An attacker
with a stolen database can try billions of password guesses per second.
bcrypt limits them to thousands per second. The math makes
brute force attacks impractical.
I used a cost factor of 12. That means 4096 iterations per hash.
The Bug That Took the Longest to Fix
I installed Express version 5 without realizing it.
In Express 4, you could write routes with inline regex like this:
app.get('/:code([a-zA-Z0-9]{6,10})', handler)
Express 5 removed this syntax completely. My server crashed
immediately on startup with a PathError that looked like this:
PathError [TypeError]: Unexpected ( at index 6: /:code([a-zA-Z0-9]{6,10})
I spent 30 minutes reading the error message before I realized
the problem was the Express version, not my regex.
The fix was downgrading to Express 4 and moving the validation
into the controller function instead:
// Now in the controller
const validCode = /^[a-zA-Z0-9]{4,20}$/.test(code);
if (!validCode) {
return res.status(404).json({ error: 'Invalid short code' });
}
One line of code. 30 minutes to find where to put it.
That experience taught me something important. When you see
a confusing error, check your dependency versions before
assuming the bug is in your logic.
The nanoid Problem
I installed nanoid version 5. My project uses CommonJS with require.
nanoid version 4 and above only supports ES Modules.
They removed CommonJS support.
When I tried to use it I got this error:Error [ERR_REQUIRE_ESM]: require() of ES Module not supported
The fix was downgrading to nanoid version 3,
the last version with CommonJS support:
npm install nanoid@3.3.6
This is the kind of thing no tutorial warns you about.
You only learn it by hitting the wall yourself.
Deploying to Railway
Deployment was the part I was most nervous about.
It turned out to be the easiest part.
Steps I took:
- Pushed code to GitHub
- Connected Railway to my GitHub repository
- Added a PostgreSQL database on Railway
- Copied database credentials into Railway environment variables
- Created the tables using Railway's built-in console
- Generated a domain
- Tested the live URL
The whole process took about 45 minutes.
The most important thing I learned about deployment:
your .env file never goes to GitHub. Ever.
Environment variables are configured separately on the server.
What the Live API Can Do
You can test it right now.
Health check:GET https://urlshortener-api-production.up.railway.app/health
Register an account:POST https://urlshortener-api-production.up.railway.app/api/auth/register
Body: { "name": "Your Name", "email": "you@example.com", "password": "password123" }
Shorten a URL after logging in:
POST https://urlshortener-api-production.up.railway.app/api/urls/shorten
Authorization: Bearer your_token_here
Body: { "url": "https://www.google.com" }
What I Learned
Backend is not about knowing frameworks. It is about understanding problems.
Before this project I thought Express was magic. Now I know
it is just a wrapper around Node's http module that makes
routing and middleware easier.
Before this project I thought JWT was complicated. Now I can
implement it from scratch without looking anything up.
Before this project I thought deployment meant buying a server.
Now I know free hosting exists and it takes less than an hour.
The five most important things I actually learned:
MVC architecture is not optional for real projects.
It is the difference between code you can maintain
and code you are afraid to touch.Never concatenate user input into SQL queries.
Always use parameterized queries.
SQL injection is real and trivially easy to exploit.Environment variables are not optional.
Your secrets never go in your code. Never.Read error messages carefully before assuming
the bug is in your logic. Half the time
it is a version conflict or a misconfiguration.Deploy early. A project that only runs on localhost
does not exist from anyone else's perspective.
What is Next
I am building a Task Manager API with teams, roles,
and email notifications as my next project.
New things I am learning: many-to-many database relationships,
role-based access control, file uploads with Multer,
and sending emails from a backend with Nodemailer.
Follow me here on dev.to or on LinkedIn if you want to
follow the journey.
If you are a CS student who has been putting off building
your first project — this post is me telling you to just start.
The gap between knowing and building is smaller than you think.
GitHub: https://github.com/devanshjaincampaign-tech/url_shortener-api
Live API: https://urlshortener-api-production.up.railway.app/health
LinkedIn: https://www.linkedin.com/in/devansh-jain-314208375/
Top comments (0)