DEV Community

Alaa Mekibes
Alaa Mekibes

Posted on

Sakani: Building an Algerian Real Estate Platform with the MERN Stack

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
Enter fullscreen mode Exit fullscreen mode
  • 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
);
Enter fullscreen mode Exit fullscreen mode

A few things I focused on:

  • Auth: JWT stored in an httpOnly cookie, so it can't be read or stolen by client-side JavaScript. Passwords are hashed with bcryptjs before ever touching the database.
  • Image uploads: multer handles incoming files, and multer-storage-cloudinary sends 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 ApiResponse helper, 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, and UnauthorizedError that 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.ts fetch wrapper, so every request in the app goes through the same place and automatically includes cookies for auth. It exposes simple methods like api.get(), api.post(), and api.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' });
}
Enter fullscreen mode Exit fullscreen mode
  • A global AuthContext that 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)