Full-stack pet adoption platform — list pets, schedule visits, and manage the entire adoption cycle with JWT authentication, image uploads, and a protected REST API.
- About The Project
- Features
- Tech Stack
- Getting Started
- Project Structure
- Architecture
- Database Schema
- API Documentation
- Challenges & Solutions
- What I Learned
- Roadmap
- License
- Author
Get a Pet is a full-stack web platform that connects people who want to adopt pets with those who have pets available for adoption. Users can register, create listings for their pets with multiple photos, and manage the full adoption lifecycle — from scheduling visits to concluding the adoption.
The project was built to go beyond simple CRUD applications and tackle real-world concerns: secure authentication with JWT, protected API routes, file upload handling, and a global auth state shared across the entire frontend via Context API.
Every route that modifies data is protected by a verifyToken middleware on the backend. The frontend mirrors this by checking auth state before rendering sensitive pages, keeping both layers consistent.
The goal was to build a complete full-stack application from scratch — designing the data models, implementing authentication, handling file uploads, and connecting everything to a React frontend. Get a Pet covers the full lifecycle of a real feature: from user registration to a concluded adoption, with proper authorization checks at every step.
- 🐾 Pet listings — Register pets with name, age, weight, color, and multiple photos
- 📅 Visit scheduling — Interested users can schedule a visit to meet a pet; the owner's contact info is returned on confirmation
- ✅ Adoption conclusion — Owners can mark an adoption as complete, making the pet unavailable
- ✏️ Pet management — Owners can edit or delete their own listings at any time
- 👤 User profile — Update personal info and profile picture
- Flash messages for feedback on every action (success and error)
- Pets marked as unavailable are visually distinguished on the listing
- Conditional UI based on auth state — navbar and pages adapt to logged-in vs. guest users
- Dedicated dashboard pages: "My Pets" and "My Adoptions"
- JWT authentication with token stored on the client and sent via
Authorizationheader verifyTokenmiddleware protecting all write operations on the API- Ownership checks before any update or delete — users can only modify their own data
- Multiple image upload per pet via Multer, served as static files from the backend
- Global auth state via React Context API + custom
useAuthhook - Event bus pattern (
useFlashMessage) for cross-component flash messages without prop drilling
Frontend:
- React 18 — UI and component structure
- React Router 6 — Client-side routing and protected routes
- Context API — Global authentication state
- Axios — HTTP client with base URL configuration
- React Icons — UI icon library
- CSS Modules — Scoped per-component styles
Backend:
- Node.js + Express — REST API and middleware pipeline
- MongoDB + Mongoose — Database and ODM
- JWT (jsonwebtoken) — Stateless authentication
- bcrypt — Password hashing
- Multer — Multipart form data and image upload handling
- CORS — Cross-origin configuration for local dev
Tools:
- Vite — Frontend dev server and build tool
- Nodemon — Backend auto-restart on file changes
- Node.js 18+
- npm
- A running MongoDB instance (local or MongoDB Atlas)
- Clone the repository
git clone https://github.com/Luan-Neumann-Dev/get-a-pet.git
cd GetAPet- Install backend dependencies
cd backend
npm install- Configure the database connection
Open backend/db/conn.js and set your MongoDB connection string:
mongoose.connect('your_mongodb_connection_string')- Start the backend server
npm start
# Runs on http://localhost:5000- Install frontend dependencies (in a new terminal)
cd frontend
npm install- Start the frontend dev server
npm run dev
# Runs on http://localhost:5173get-a-pet/
│
├── backend/
│ ├── controllers/
│ │ ├── UserController.js # Register, login, profile management
│ │ └── PetController.js # Full pet CRUD + schedule + conclude
│ ├── helpers/
│ │ ├── create-user-token.js # JWT generation and response
│ │ ├── verify-token.js # Auth middleware for protected routes
│ │ ├── get-token.js # Extracts token from Authorization header
│ │ ├── get-user-by-token.js # Resolves user from token
│ │ └── image-upload.js # Multer configuration
│ ├── models/
│ │ ├── User.js # User schema
│ │ └── Pet.js # Pet schema
│ ├── routes/
│ │ ├── UserRoutes.js
│ │ └── PetRoutes.js
│ ├── db/conn.js # MongoDB connection
│ └── index.js # Express app entry point
│
└── frontend/
└── src/
├── components/
│ ├── form/ # Input, Select, PetForm
│ └── layout/ # Navbar, Footer, Message, RoundedImage
├── pages/
│ ├── Auth/ # Login, Register
│ ├── Pet/ # AddPet, EditPet, PetDetails, MyPets, MyAdoptions
│ └── User/ # Profile
├── context/
│ └── UserContext.jsx # Global auth state
├── hooks/
│ ├── useAuth.jsx # Auth state consumer hook
│ └── useFlashMessage.jsx # Flash message event bus
├── utils/
│ ├── api.js # Axios instance with base URL
│ └── bus.js # Event emitter for flash messages
└── App.jsx # Router and layout setup
React (Context API) → Axios (api.js) → Express Router → Middleware (verifyToken) → Controller → Mongoose → MongoDB
User submits login → UserController validates credentials → bcrypt compares password
→ JWT signed and returned → Frontend stores token → Sent in Authorization header on every request
→ verifyToken middleware decodes token → Controller resolves user via getUserByToken
Owner creates pet (available: true)
→ Visitor schedules visit (adopter set on pet document)
→ Owner concludes adoption (available: false)
- name String (required)
- email String (required)
- password String (required, bcrypt hashed)
- phone String (required)
- image String (filename, optional)
- timestamps
- name String (required)
- age Number (required)
- weight Number (required)
- color String (required)
- images Array (filenames, required)
- available Boolean (default: true)
- user Object { _id, name, image, phone }
- adopter Object { _id, name, image }
- timestamps
User (1) ──→ (N) Pet [as owner via pet.user._id]
User (1) ──→ (N) Pet [as adopter via pet.adopter._id]
POST /users/register
POST /users/login| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /users/checkUser |
Get current user from token | No |
| GET | /users/:id |
Get user by ID | No |
| PATCH | /users/edit/:id |
Update profile + photo | Yes |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /pets |
List all pets | No |
| GET | /pets/:id |
Get pet details | No |
| POST | /pets/create |
Create new pet listing | Yes |
| PATCH | /pets/:id |
Update pet | Yes (owner only) |
| DELETE | /pets/:id |
Remove pet | Yes (owner only) |
| GET | /pets/mypets |
List authenticated user's pets | Yes |
| GET | /pets/myadoptions |
List user's scheduled adoptions | Yes |
| PATCH | /pets/schedule/:id |
Schedule a visit | Yes |
| PATCH | /pets/conclude/:id |
Conclude adoption | Yes (owner only) |
curl -X POST http://localhost:5000/users/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "yourpassword"}'Problem: Multiple pages and components needed to know if the user was logged in, who they were, and react to changes in real time (login, logout, profile update).
Solution: Implemented a UserContext with React's Context API, wrapping the entire app. A custom useAuth hook exposes auth state and the token cleanly to any component.
const { authenticated, user } = useAuth()Result: No prop drilling for auth state. Any component can consume auth context directly.
Problem: Flash messages needed to be triggered from deep inside a form component and displayed in a layout component at the top of the tree — with no shared parent to pass props through.
Solution: Built a lightweight event bus using Node's EventEmitter (via the events package). The useFlashMessage hook emits events, and the Message component subscribes to them independently.
// Emit from any component
const { createMessage } = useFlashMessage()
createMessage({ type: 'success', msg: 'Pet criado com sucesso!' })Result: Fully decoupled flash message system with no prop drilling, working across any component depth.
Problem: Any authenticated user could technically call PATCH /pets/:id or DELETE /pets/:id on a pet they don't own.
Solution: After validating the JWT with verifyToken, every write controller resolves the requesting user from the token and compares their _id against the pet's user._id before allowing the operation.
if (pet.user._id.toString() !== user._id.toString()) {
return res.status(422).json({ message: "Acesso negado." })
}Result: Users can only modify or delete resources they own, regardless of the ID passed in the URL.
Technical Skills:
- Full-stack integration: Connecting a React frontend to an Express REST API with Axios, managing base URLs and auth headers centrally via an Axios instance
- JWT authentication: Generating tokens on login/register, verifying them in middleware, and extracting user identity on protected routes
- File upload pipeline: Handling
multipart/form-datawith Multer, storing files on disk, and serving them as static assets from Express - MongoDB data modeling: Embedding related data (user info inside pet documents) as a deliberate denormalization for read performance
Best Practices:
- Centralizing auth logic in middleware instead of repeating checks across controllers
- Separating concerns with helper functions (
getToken,getUserByToken,createUserToken) to keep controllers clean - Using Context API for global state instead of lifting state up through unrelated component layers
Soft Skills:
- Planning a full-stack feature end-to-end before writing code — modeling the data first, then the API, then the UI
- Recognizing when embedding vs. referencing in MongoDB is the right call
- Search and filter pets by species, age, or location
- Real-time chat between owner and potential adopter
- Email notifications when a visit is scheduled
- Pagination on the pet listing page
- Migrate to TypeScript on both frontend and backend
- Add input validation with Zod or Joi on the API
- Replace the hardcoded JWT secret with a proper environment variable
- Add unit tests for controllers and helpers
- Passwords hashed with bcrypt (12 salt rounds) — never stored in plain text
- JWT used for stateless authentication — no sessions or cookies
- All write operations protected by
verifyTokenmiddleware - Ownership verified on every update and delete before any DB operation
- Passwords excluded from all API responses via
select('-password')and manualundefinedassignment
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Feel free to open an issue or submit a Pull Request.
- Fork the project
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Luan Neumann
⭐ Star this repository if you found it helpful!
Made with ❤️ and ☕ by Luan Neumann


