
The Wild Oasis - Custom Backend
The supabase problem
so here's what happened — I had the wild oasis project running on supabase's free tier for a while. it was working fine, until one day my project just... expired. the free tier just kills your project if it's inactive for too long. classic.
at first I was annoyed, but then I thought wait — this is actually a great excuse to build something real. instead of just re-deploying on supabase or switching to another BaaS, why not build the entire backend myself? learn how stuff actually works under the hood.
and that's exactly what I did.
The inspiration — youtube-twitter backend
I had already gone through hitesh choudhary's youtube-twitter backend project, and I really liked the patterns he used there. proper industry standard stuff — structured error handling, consistent API responses, clean separation of concerns. so I took that as my base and adapted it for the wild oasis.
the core idea was simple: if I'm going to build a backend, I'm going to do it the right way. scalable, secure, and maintainable.
Project structure
here's how the project is organized:
the-wild-oasis-backend/
├── src/
│ ├── app.js # express app setup + middleware
│ ├── index.js # server entry point
│ ├── constants.js
│ ├── swagger.js # API docs config
│ ├── controllers/
│ │ ├── bookings.controller.js
│ │ ├── cabins.controller.js
│ │ ├── guests.controller.js
│ │ ├── settings.controller.js
│ │ └── user.controller.js
│ ├── models/
│ │ ├── bookings.model.js
│ │ ├── cabins.model.js
│ │ ├── guest.model.js
│ │ ├── settings.model.js
│ │ └── user.model.js
│ ├── routes/
│ │ ├── bookings.routes.js
│ │ ├── cabins.routes.js
│ │ ├── guests.routes.js
│ │ ├── settings.routes.js
│ │ └── user.routes.js
│ ├── middlewares/
│ │ ├── auth.middleware.js
│ │ └── multer.middleware.js
│ ├── utils/
│ │ ├── ApiError.js
│ │ ├── ApiResponse.js
│ │ ├── asyncHandler.js
│ │ └── cloudinary.js
│ ├── db/
│ │ └── index.js
│ └── scripts/
│ └── seed.js
├── public/
└── package.jsonclean MVC-ish pattern. controllers handle the logic, models define the data, routes wire everything up. nothing fancy, just solid structure that scales.
The utils — where the magic lives
this is straight from the hitesh choudhary playbook and honestly it's one of the best patterns I've picked up.
ApiError
custom error class that gives you consistent error responses across the entire app:
class ApiError extends Error {
constructor(
statusCode,
message = "something went wrong",
error = [],
stack = "",
) {
super(message);
this.statusCode = statusCode;
this.data = null;
this.message = message;
this.success = false;
this.error = error;
if (stack) {
this.stcak = stack;
} else {
Error.captureStackTrace(this, this.constructor);
}
}
}now whenever something goes wrong in any controller, I just throw new ApiError(404, "cabin not found") and the global error handler catches it. no more scattered res.status().json() calls everywhere.
ApiResponse
same idea but for success responses:
class ApiResponse {
constructor(statusCode, data, message = "success") {
this.statusCode = statusCode;
this.data = data;
this.message = message;
this.success = statusCode < 400;
}
}every successful response looks the same — statusCode, data, message, and a success boolean. the frontend always knows what to expect.
asyncHandler
this tiny wrapper saved me from writing try-catch in every single controller:
const asyncHandler = (requestHandler) => {
return (req, res, next) => {
Promise.resolve(requestHandler(req, res, next)).catch((err) => next(err));
};
};wraps any async function and forwards errors to express's error handler automatically. simple but super effective.
security and middleware
I went all in on security for this one. the app.js is loaded with production-grade middleware:
// Security HTTP Headers
app.use(helmet());
// Rate limiting
const limiter = rateLimit({
max: 100,
windowMs: 60 * 60 * 1000,
message: "Too many requests from this IP, please try again in an hour!",
});
app.use("/api", limiter);
// Data sanitization against NoSQL query injection
app.use(mongoSanitize());- helmet — sets a bunch of security HTTP headers
- rate limiting — 100 requests per hour per IP, no one's DDoS-ing my hotel API
- mongo sanitize — prevents NoSQL injection attacks
- CORS — configured for both the admin dashboard (vite on 5173) and the customer website (next.js on 3000)
- swagger — full API documentation at
/api-docs
refactoring the frontends
this was honestly the hardest part. the original wild oasis project by jonas uses supabase directly from the frontend — no API layer at all. so when I built this custom backend, I had to refactor both the admin dashboard and the customer-facing website to talk to my API instead.
every supabase call had to be ripped out and replaced with fetch/axios calls to my express API. the data shapes were different too since I'm using mongoose models now instead of supabase's postgres tables. lots of mapping and adapting.
but it was worth it. now the frontends are properly decoupled from the database, which is how it should be in production anyway.
tech stack
| technology | purpose |
|---|---|
| express.js | API framework |
| mongodb + mongoose | database + ODM |
| JWT + bcrypt | auth + password hashing |
| cloudinary | image/media storage |
| helmet | security headers |
| swagger | API documentation |
| multer | file upload handling |
| zod | request validation |
what I learned
building this backend taught me way more than any tutorial could. some takeaways:
- BaaS is convenient, but knowing how to build your own backend is invaluable. when supabase expired, I didn't panic — I just built my own.
- consistent error handling patterns make everything cleaner. the ApiError/ApiResponse pattern from hitesh's project is something I now use in every backend I build.
- security isn't optional. rate limiting, helmet, sanitization — these are table stakes for any production API.
- refactoring is where the real learning happens. tearing out supabase and wiring up a custom API forced me to understand every data flow in the app.
conclusion
what started as a "damn, supabase killed my project" moment turned into one of the best learning experiences I've had. the project is now fully self-hosted, scalable, and doesn't depend on any third-party BaaS that could expire on me again.
if you're sitting on a project that uses supabase or firebase and you want to level up — try building the backend yourself. it's painful at first but it's worth every line of code.
- Source: GitHub Repository
Comments