Avsa Studio is a full-stack photography booking platform built with the PERN stack. Clients can explore photography services, review pricing and locations, and submit a session request. A protected admin workspace allows the studio to review requests, update booking statuses, and manage business content.
The visual direction combines an editorial portfolio with a practical booking system, so the application works as both a marketing website and an internal studio tool.
- Give clients a clear way to discover and request photography services.
- Prevent invalid, past, or conflicting booking requests.
- Provide administrators with protected booking and content management.
- Demonstrate a complete PERN data flow and RESTful CRUD architecture.
- Keep the frontend, API, and database separated so each layer can evolve independently.
PERN describes the four main technologies used by the project:
- PostgreSQL stores bookings, services, availability, locations, reviews, gallery records, and contact messages.
- Express defines the REST API, middleware, validation, and error handling.
- React renders the customer website, booking flow, navigation, and admin workspace.
- Node.js runs the Express server and database scripts.
Vite is used as the React development and production build tool.
The project follows a three-layer architecture:
Browser / React
│
│ HTTP + JSON
▼
Express REST API
│
│ Parameterized SQL
▼
PostgreSQL
The React frontend is responsible for:
- Rendering pages and reusable UI components.
- Managing form and navigation state.
- Performing browser-side validation.
- Sending API requests with
fetch. - Displaying success and error feedback.
- Storing the temporary admin token in
sessionStorage.
The frontend never connects directly to PostgreSQL. It communicates exclusively through the Express API.
The Express backend is responsible for:
- Defining REST endpoints.
- Validating incoming request data.
- Protecting administrative operations.
- Checking booking conflicts and availability.
- Executing parameterized database queries.
- Returning consistent JSON responses and HTTP status codes.
- Passing unexpected errors to centralized error-handling middleware.
This keeps business rules out of the React components and prevents database credentials from being exposed to the browser.
PostgreSQL is responsible for persistent and relational data. The schema uses:
- Primary keys for record identity.
- Required fields and data types.
- A booking-status constraint.
- Rating validation for reviews.
- A unique availability constraint.
- A partial unique index for confirmed booking slots.
- Timestamps for booking and contact history.
avsa-studio/
├── frontend/
│ └── src/
│ ├── api/ # Browser API request helpers
│ ├── assets/ # Photography and visual assets
│ ├── components/ # Reusable navigation, cards, and forms
│ ├── pages/ # Customer and admin page views
│ ├── App.jsx # Navigation and page composition
│ ├── main.jsx # React entry point
│ └── styles.css # Responsive editorial design system
├── backend/
│ ├── api/ # Express resource and booking routes
│ ├── db/
│ │ ├── client.js # PostgreSQL connection pool
│ │ ├── schema.sql # Database schema and constraints
│ │ └── seed.js # Schema initialization and seed data
│ ├── middleware/
│ │ ├── auth.js # Admin token authorization
│ │ └── errorHandler.js
│ └── server.js # Express application entry point
└── .env.example
CRUD stands for Create, Read, Update, and Delete. These operations are exposed through RESTful API endpoints.
| CRUD operation | HTTP method | Example endpoint | Project use |
|---|---|---|---|
| Create | POST |
/api/bookings |
Submit a booking request |
| Read | GET |
/api/services |
Display available services |
| Update | PUT |
/api/bookings/:id |
Change a booking status |
| Delete | DELETE |
/api/gallery/:id |
Remove a gallery record |
The resource API supports CRUD for:
- Services
- Locations
- Availability
- Gallery entries
- Reviews
- Contact messages
Bookings have a specialized controller because they require additional validation and conflict detection.
When a client submits the booking form:
- React collects the name, email, service, date, time, location, and notes.
- The frontend sends a
POST /api/bookingsrequest containing JSON. - Express checks required fields and rejects past dates.
- PostgreSQL obtains a transaction-level lock for the requested slot.
- The API checks existing inquiries, confirmed bookings, and unavailable times.
- If the slot is valid, the booking is inserted and the transaction is committed.
- Express returns the created booking with
201 Created. - React displays confirmation to the client.
The transaction and advisory lock prevent two simultaneous requests from reserving the same location and time.
Administrative requests include:
Authorization: Bearer <ADMIN_TOKEN>The authorization middleware compares the supplied token with the server-side environment value. Protected operations include:
- Reading customer booking records.
- Updating booking statuses.
- Deleting bookings.
- Creating, updating, or deleting business resources.
Public clients can read published business data and submit booking or contact requests without receiving administrative access.
| Resource | Base endpoint | Public behavior | Protected behavior |
|---|---|---|---|
| Bookings | /api/bookings |
Create | Read, update, delete |
| Services | /api/services |
Read | Create, update, delete |
| Locations | /api/locations |
Read | Create, update, delete |
| Availability | /api/availability |
Read | Create, update, delete |
| Gallery | /api/gallery |
Read | Create, update, delete |
| Reviews | /api/reviews |
Read | Create, update, delete |
| Contacts | /api/contacts |
Create | Read, update, delete |
All database values are passed through PostgreSQL query parameters. Resource and column names come from server-defined allowlists rather than user input.
- Admin endpoints require a bearer token.
- The token is stored in an environment file instead of source control.
- SQL values use parameterized queries.
- Writable resource fields are restricted by allowlists.
- Booking statuses are validated in both Express and PostgreSQL.
- Booking creation uses a database transaction.
- Concurrent requests for the same slot use an advisory lock.
- Confirmed booking slots have an additional database uniqueness rule.
- CORS restricts browser requests to the configured frontend origin.
- Helmet adds standard HTTP security headers.
- Errors are handled by centralized middleware.
For a production system, the token-based admin login would be replaced with user accounts, password hashing, short-lived sessions or JWTs, role-based authorization, rate limiting, and CSRF protection where applicable.
- Node.js
- npm
- PostgreSQL
createdb avsa_studiocp .env.example backend/.envUpdate backend/.env:
DATABASE_URL=postgresql://localhost/avsa_studio
CLIENT_URL=http://localhost:5173
PORT=3001
ADMIN_TOKEN=replace-with-a-long-random-secretnpm run install:allnpm --prefix backend run db:seednpm run dev- Frontend:
http://localhost:5173 - API health check:
http://localhost:3001/api/health - Admin workspace:
http://localhost:5173/admin
Sign into the admin workspace with the value configured as ADMIN_TOKEN.
npm run buildThe compiled frontend is written to frontend/dist.
Avsa Studio is a PERN photography booking platform. React provides the client-facing portfolio, booking form, and protected admin interface. Express exposes RESTful CRUD endpoints and contains validation and booking rules. PostgreSQL stores the relational business data and enforces additional constraints. I separated the presentation, application, and data layers so the UI never accesses the database directly. One problem I specifically handled was concurrent booking requests, using a PostgreSQL transaction and advisory lock to prevent the same time slot from being requested twice.
Why PostgreSQL?
The application contains structured, related business records that benefit from constraints, transactions, and reliable conflict checking. PostgreSQL provides stronger data integrity than keeping this information only in frontend state or flat files.
Why separate booking routes from generic resource routes?
Services and gallery records mostly need standard CRUD behavior. Booking creation has business-specific rules such as date validation, availability checks, conflict detection, and transactions, so it has a dedicated controller.
How is the frontend separated from the backend?
React communicates through HTTP and JSON. API helper modules centralize the backend URL, headers, response parsing, and error handling. The Express server owns all database access and business rules.
How are race conditions handled?
The API creates a transaction and requests a PostgreSQL advisory lock derived from the booking date, time, and location. Requests for the same slot are therefore checked sequentially rather than both passing the conflict query at the same moment.
What would you improve next?
- Replace the shared admin token with account-based authentication.
- Add automated unit and integration tests.
- Add migrations for versioned schema changes.
- Store uploaded gallery images in object storage.
- Connect services and bookings through foreign keys.
- Calculate conflicts using service duration rather than only start time.
- Add email confirmation and payment processing.
- Add pagination, filtering, and an audit log to the admin workspace.
- Admin authorization is intentionally lightweight for this portfolio project.
- Navigation uses the browser History API without an additional routing dependency.
- Booking conflicts currently compare the selected start time and location.
- Service names and locations are stored on booking records as snapshots rather than foreign keys.
- The UI includes temporary image slots that will be replaced with final portfolio photography.