I built Sakani as a learning project to practice a full MERN stack setup end to end: a React frontend talking to a Node/Express/MongoDB backend, with proper auth, image uploads, and validation on both sides.
What Sakani does
It's a real estate listing platform. Users can sign up, browse properties with filters, view property details, and if they're logged in, list their own properties, edit them, and receive inquiries from interested buyers.
The backend: Express, MongoDB, and MVC
The backend is a REST API built with Node.js, Express 5, TypeScript, and MongoDB through Mongoose. I structured it around the classic MVC pattern, adapted for an API where there's no view layer, just JSON responses:
Request → Route → Middleware → Controller → Model → Database
↓
JSON Response
-
Models (
src/models/) define the MongoDB schemas:user,property,inquiry. -
Controllers (
src/controllers/) hold the business logic. They never touch the database directly, they always go through a model. -
Routes (
src/routes/) map HTTP methods and paths to controllers, and chain middleware in front of them. -
Middleware (
src/middleware/) handles auth checks, Zod validation, file uploads, and error handling, all reusable across routes.
Here's what a typical route looks like, stacking middleware before the actual logic runs:
router.post('/',
authMiddleware, // 1. verify JWT
upload.array('images'), // 2. handle file upload
validate(schema), // 3. validate body with Zod
propertyController.create // 4. run business logic
);
A few things I focused on:
-
Auth: JWT stored in an
httpOnlycookie, so it can't be read or stolen by client-side JavaScript. Passwords are hashed withbcryptjsbefore ever touching the database. -
Image uploads:
multerhandles incoming files, andmulter-storage-cloudinarysends them straight to Cloudinary without saving anything to local disk first. -
Validation: every request body, query, and param that matters gets checked with Zod through a small reusable
validate()middleware. -
Consistent responses: every endpoint returns the same shape through an
ApiResponsehelper, so the frontend always knows what to expect, whether it's a success, a validation failure, or an error. -
Custom errors: instead of scattering status codes everywhere, I use error classes like
NotFoundError,ConflictError, andUnauthorizedErrorthat a global error handler catches and turns into proper responses.
The frontend: React, TanStack Router, and TypeScript
The frontend is React 19 with TypeScript, built with Vite, and it leans heavily on the TanStack ecosystem:
-
TanStack Router for file-based routing. The folder structure under
src/routes/defines the actual routes, and a Vite plugin auto-generates the route tree on every save. - TanStack Form for handling forms like create/update property and login/signup, paired with Zod for validation.
- TailwindCSS + DaisyUI for styling, so I get ready-made components like navbars and cards without writing everything from scratch.
Some of the pieces I'm proud of:
-
A centralized
api.tsfetch wrapper, so every request in the app goes through the same place and automatically includes cookies for auth. It exposes simple methods likeapi.get(),api.post(), andapi.upload()for multipart image uploads. - Route-level auth protection, where a route simply declares what it needs and gets redirected if the condition fails:
beforeLoad: ({ context }) => {
if (!context.isAuthenticated) throw redirect({ to: '/login' });
}
-
A global
AuthContextthat fetches the current user on app load to restore the session from the cookie, so refreshing the page doesn't log you out. - Search param validation, so filters on the properties page are validated with a Zod schema, and anything invalid is just silently ignored instead of crashing the page.
How the two sides talk to each other
The frontend never touches MongoDB directly, obviously. Every interaction goes through the REST API: things like listing properties with filters, creating a property with images, or sending an inquiry to a property owner. The API responses are typed on the frontend through shared interfaces, so what the backend sends and what the frontend expects stay in sync.
What I learned
- How to structure a real backend around MVC instead of dumping everything into route handlers
- How to handle authentication properly with JWT in httpOnly cookies instead of localStorage
- How to wire up image uploads that go straight to a cloud storage service without touching my own server's disk
- How file-based routing works in TanStack Router and how to protect routes based on auth state
- How to keep validation consistent by using the same Zod schemas as the single source of truth for what "valid data" means
- How much smoother frontend and backend development get when responses follow one consistent shape
Try it yourself
If you're learning MERN, I'd recommend picking a real-world domain like this one (listings, bookings, marketplaces) since it naturally forces you to deal with auth, file uploads, filtering, and relationships between resources, all the stuff that makes a project feel like more than a to-do list.
Frontend: github.com/alaa-mekibes/sakani-frontend-react
Backend: github.com/alaa-mekibes/sakani-backend-mongoose 🏠
Top comments (0)