A production-minded REST API for a laundry and dry-cleaning pickup-and-delivery marketplace.
QuickServe is a backend platform that connects customers, riders, dry-cleaning partners, and operations staff through a single API.
It manages the complete order journey, from service selection and order placement to payment, pickup, cleaning, delivery, disputes, and partner payouts.
The project is built with Node.js, Express, PostgreSQL, and Knex.js, with a strong focus on clean architecture, validation, authentication, authorization, and state-driven business logic.
QuickServe is designed around a simple idea:
Make booking laundry and dry-cleaning services as convenient and trackable as ordering a ride or food.
The backend coordinates multiple actors while keeping the business rules centralized and secure.
┌──────────────────┐
│ Customer │
└────────┬─────────┘
│
▼
┌──────────────┐ ┌───────────────┐ ┌──────────────────┐
│ Rider │◄────►│ QuickServe API│◄────►│ Dry-Clean Partner│
└──────────────┘ └───────┬───────┘ └──────────────────┘
│
▼
┌─────────────────┐
│ Operations/Admin│
└─────────────────┘
The API is responsible for coordinating these actors while enforcing the rules that determine what each actor can see and do.
QuickServe was built as a hands-on backend engineering project focused on solving a realistic marketplace problem.
Rather than treating the API as a collection of CRUD endpoints, the project applies production-oriented patterns such as:
- Layered architecture
- Domain-based module organization
- JWT authentication
- Role-based authorization
- Request validation with Zod
- Server-side pricing
- Database-backed state transitions
- Transactional database operations
- Payment webhook verification
- Centralized error handling
- Cloud-based media storage
- Audit-friendly order history
The goal is to build a backend that is predictable, testable, secure, and capable of evolving beyond an MVP.
- Account registration and login
- Email verification
- Password reset
- Access and refresh token authentication
- Address management
- Service catalog
- Order creation
- Order tracking
- Payment through Paystack
- Order cancellation
- Dispute creation
- Customer support messaging
- Device registration for future push notifications
- Rider authentication
- Assigned task management
- Pickup and delivery workflow
- Order status updates
- Proof-of-handoff uploads
- Partner authentication
- Assigned order queue
- Order acceptance
- Cleaning status management
- SLA visibility
- Operations messaging
- Order monitoring
- Rider assignment
- Partner assignment
- Partner approval and management
- Dispute management
- Payout management
- SLA monitoring
- Role and tier-based access control
- PostgreSQL persistence
- Database migrations with Knex
- Server-side pricing
- Service-zone validation
- Paystack payments and webhooks
- Cloudinary media storage
- Transactional email
- Centralized API errors
- Append-only order status history
| Category | Technology |
|---|---|
| Runtime | Node.js |
| Framework | Express 5 |
| Language | JavaScript |
| Modules | ES Modules |
| Database | PostgreSQL |
| Query Builder | Knex.js |
| Validation | Zod |
| Authentication | JWT |
| Password Hashing | bcrypt |
| File Uploads | Multer |
| Media Storage | Cloudinary |
| Payments | Paystack |
| Nodemailer + Gmail SMTP | |
| API Hosting | Render |
| Database Hosting | Neon |
QuickServe follows a module-per-domain architecture.
The application is organized around business domains rather than technical layers alone.
A typical request flows through:
HTTP Request
│
▼
Route
│
▼
Middleware
│
├── Authentication
├── Authorization
└── Validation
│
▼
Controller
│
▼
Service
│
├── PostgreSQL
├── Paystack
├── Cloudinary
└── Email
│
▼
JSON Response
Routes define the public HTTP interface and middleware chain.
They should remain declarative and lightweight.
Controllers translate HTTP requests into service calls and format responses.
They should not contain complex business logic.
Services contain the application's business rules.
Examples include:
- Creating an order
- Calculating an order price
- Assigning a rider
- Validating an order transition
- Resolving a dispute
- Processing a payment
- Creating a payout
Middleware handles cross-cutting concerns such as:
- Authentication
- Authorization
- Request validation
- Error handling
This separation keeps the business logic independent from Express.
quickserve-backend/
│
├── src/
│ │
│ ├── app.js
│ │
│ ├── config/
│ │ ├── db.js
│ │ ├── cloudinary.js
│ │ ├── paystack.js
│ │ └── email.js
│ │
│ ├── db/
│ │ ├── migrations/
│ │ └── seeds/
│ │
│ ├── modules/
│ │ ├── auth/
│ │ ├── addresses/
│ │ ├── orders/
│ │ ├── payments/
│ │ ├── riders/
│ │ ├── riderTasks/
│ │ ├── partners/
│ │ ├── partnerOrders/
│ │ ├── adminOrders/
│ │ ├── adminPartners/
│ │ ├── adminDisputes/
│ │ ├── adminPayouts/
│ │ ├── messages/
│ │ ├── notifications/
│ │ └── devices/
│ │
│ ├── middleware/
│ │ ├── authenticate.js
│ │ ├── authorize.js
│ │ ├── authorizeTier.js
│ │ ├── validate.js
│ │ └── errorHandler.js
│ │
│ ├── services/
│ │ └── orderTransitions.service.js
│ │
│ └── utils/
│ ├── formatters.js
│ ├── geo.js
│ ├── cloudinaryStorage.js
│ ├── paystackClient.js
│ └── email.js
│
├── .env.example
├── .gitignore
├── knexfile.js
├── package.json
├── server.js
└── README.md
Each domain owns its routes, controllers, services, and validation where applicable.
This makes features easier to reason about and reduces the risk of business logic becoming scattered across the application.
Orders are not treated as arbitrary records whose status can be changed freely.
QuickServe uses a state-machine-driven order pipeline.
order_placed
│
▼
rider_assigned
│
▼
picked_up
│
▼
at_hub
│
▼
sent_to_partner
│
▼
at_partner
│
▼
cleaning_in_progress
│
▼
ready_for_pickup
│
▼
returned_to_hub
│
▼
out_for_delivery
│
▼
delivered
Two additional states are used outside the normal pipeline:
cancelled
disputed
Without a state machine, a client could potentially request invalid changes such as:
order_placed → delivered
or:
delivered → cleaning_in_progress
QuickServe prevents this by validating every transition against explicit server-side rules.
The transition system acts as a guardrail around the order lifecycle.
Current Status
+
Requested Status
+
Actor Role
│
▼
Transition Rules
│
┌────┴────┐
│ │
Allowed Rejected
│
▼
Database Transaction
│
├── Update order
└── Write status history
Every successful status transition is recorded in an append-only order_status_history table.
This provides an audit trail that can be used to understand:
- What happened to an order
- When the change happened
- Which actor initiated it
- How the order reached its current state
QuickServe uses JWT access and refresh tokens.
Authenticated requests use:
Authorization: Bearer <accessToken>The backend distinguishes between:
- Authentication: Who is making the request?
- Authorization: Is that user allowed to perform this action?
A protected route can therefore follow:
authenticate
↓
authorize(role)
↓
authorizeTier(permission)
↓
validate(request)
↓
controller
Authorization is enforced server-side.
The frontend is never trusted to enforce permissions by itself.
customer
rider
partner
admin
Administrative access can be further restricted through tiers such as:
dispatcher
support
finance
super_admin
Development:
http://localhost:3000/api/v1
Production:
https://<production-domain>/api/v1
API versioning is used to provide a stable contract as the platform evolves.
The API uses camelCase.
Database columns use snake_case.
Example:
{
"orderId": "ord_123",
"currentStatus": "cleaning_in_progress",
"createdAt": "2026-08-26T08:00:00.000Z"
}Errors follow a consistent structure:
{
"message": "Human-readable error description",
"error": "VALIDATION_ERROR",
"statusCode": 400
}Common error categories include:
VALIDATION_ERROR
AUTHENTICATION_ERROR
AUTHORIZATION_ERROR
NOT_FOUND
CONFLICT
INVALID_STATE_TRANSITION
PAYMENT_ERROR
EXTERNAL_SERVICE_ERROR
INTERNAL_SERVER_ERROR
| Method | Endpoint | Description |
|---|---|---|
POST |
/auth/signup |
Register customer |
POST |
/auth/login |
Customer login |
POST |
/auth/refresh |
Refresh access token |
GET |
/auth/verify |
Verify account |
POST |
/auth/forgot-password |
Request password reset |
POST |
/auth/reset-password |
Reset password |
| Method | Endpoint | Description |
|---|---|---|
GET |
/orders |
List customer orders |
POST |
/orders |
Create an order |
GET |
/orders/:orderId |
Get order details |
POST |
/orders/:orderId/cancel |
Cancel an eligible order |
POST |
/orders/:orderId/disputes |
Raise a dispute |
GET |
/orders/:orderId/status-history |
View order history |
| Method | Endpoint | Description |
|---|---|---|
POST |
/orders/:orderId/pay |
Initialize payment |
GET |
/orders/:orderId/payment-status |
Check payment status |
POST |
/payments/webhook |
Receive Paystack webhook |
| Method | Endpoint | Description |
|---|---|---|
POST |
/rider-auth/login |
Rider login |
GET |
/rider/tasks |
List assigned tasks |
GET |
/rider/tasks/:orderId |
View task |
POST |
/rider/tasks/:orderId/status |
Update task status |
POST |
/rider/tasks/:orderId/proof |
Upload proof |
| Method | Endpoint | Description |
|---|---|---|
POST |
/partner-auth/login |
Partner login |
GET |
/partner/orders |
List assigned orders |
GET |
/partner/orders/:orderId |
View partner order |
POST |
/partner/orders/:orderId/accept |
Accept order |
POST |
/partner/orders/:orderId/status |
Update cleaning status |
GET |
/partner/orders/:orderId/sla |
View SLA information |
| Method | Endpoint | Description |
|---|---|---|
POST |
/admin-auth/login |
Admin login |
GET |
/admin/orders |
List and filter orders |
POST |
/admin/orders/:orderId/assign-rider |
Assign rider |
POST |
/admin/orders/:orderId/assign-partner |
Assign partner |
GET |
/admin/disputes |
List disputes |
PUT |
/admin/disputes/:disputeId/resolve |
Resolve dispute |
GET |
/admin/payouts |
List payouts |
PUT |
/admin/payouts/:payoutId/mark-paid |
Mark payout as paid |
The API route map is intentionally high-level. Detailed request and response schemas can be maintained in dedicated API documentation as the modules mature.
The client never determines the final order price.
Prices are resolved from the service catalog on the backend.
Client request
↓
Service selection
↓
Server-side catalog lookup
↓
Price calculation
↓
Order total
This prevents clients from manipulating prices.
Order transitions are controlled through explicit rules rather than scattered if statements across controllers.
This keeps the order lifecycle centralized and easier to test.
Important multi-step operations use database transactions where consistency matters.
For example:
Update order status
+
Create status history
should succeed together or fail together.
A dispute should not destroy the operational history of an order.
The system preserves the order's previous state so that operations can resolve the dispute and return the order to the appropriate workflow.
QuickServe currently stores service zones as JSONB polygons and performs point-in-polygon checks in application code.
This keeps the initial implementation relatively lightweight while leaving room for a more advanced geospatial implementation later.
When an order reaches delivered, the backend can create the corresponding partner payout record automatically.
This removes the need for operations staff to manually initiate every payout.
QuickServe integrates with Paystack for payment processing.
The payment flow is designed around server-side verification.
Customer
│
▼
QuickServe
│
▼
Paystack
│
▼
Webhook
│
▼
Signature Verification
│
▼
Transaction Verification
│
▼
Order Payment Update
The backend does not trust a client-side payment result as the final source of truth.
Webhook processing should also be idempotent so that duplicate webhook deliveries do not create duplicate financial records.
Multer is used to process incoming file uploads.
Cloudinary provides persistent media storage.
Client
│
▼
Multer
│
▼
Memory
│
▼
Cloudinary
│
▼
Media URL
Uploads are primarily used for rider proof-of-handoff images.
Upload endpoints should validate:
- File type
- File size
- Authentication
- Authorization
- Resource ownership
Create a .env file in the project root.
PORT=3000
NODE_ENV=development
APP_BASE_URL=http://localhost:3000
DATABASE_URL=postgres://user:password@localhost:5432/quickserve
JWT_ACCESS_SECRET=your_long_random_secret
JWT_REFRESH_SECRET=your_different_long_random_secret
PAYSTACK_SECRET_KEY=sk_test_xxxxx
PAYSTACK_PUBLIC_KEY=pk_test_xxxxx
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
GMAIL_USER=your-email@gmail.com
GMAIL_APP_PASSWORD=your16characterapppasswordNever commit .env or production credentials to Git.
Use .env.example as the public configuration template.
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"Generate different values for the access and refresh token secrets.
Make sure you have:
- Node.js 18+
- npm
- PostgreSQL 14+
- Git
For full functionality, you will also need accounts for:
- Paystack
- Cloudinary
- Gmail SMTP
Test credentials are sufficient for local development.
Clone the repository:
git clone <repository-url>
cd quickserve-backendInstall dependencies:
npm installCreate your environment file:
cp .env.example .envOn Windows, you can create the .env file manually and copy the values from .env.example.
Run the latest migrations:
npx knex migrate:latestSeed required reference data:
npx knex seed:runThe order transition rules must be seeded for the order state machine to operate correctly.
Start the development server:
npm run devOr start the server directly:
node server.jsThe API should now be available at:
http://localhost:3000
Check the health endpoint:
curl http://localhost:3000/healthWhen adding a feature, follow the domain architecture:
1. Define the business rule
↓
2. Create validation schema
↓
3. Implement service logic
↓
4. Add controller
↓
5. Add route
↓
6. Add migration if needed
↓
7. Test the complete flow
A new feature should answer:
- Who can use it?
- What data does it accept?
- What business rules apply?
- What database records change?
- Does it require a transaction?
- Does it call an external service?
- Does it need an audit record?
- Does it trigger a notification?
The project is intended to support multiple testing layers.
Good candidates include:
- Order transition rules
- Price calculation
- Service-zone calculations
- Permission checks
- Data formatting
Examples:
- Creating an order
- Assigning a rider
- Assigning a partner
- Resolving a dispute
- Creating a payout
- Processing a payment
Examples:
POST /api/v1/auth/login
POST /api/v1/orders
POST /api/v1/orders/:orderId/pay
POST /api/v1/admin/orders/:orderId/assign-rider
Every valid transition should have a success test.
Every invalid transition should have a rejection test.
Critical cases should also verify:
- Correct actor permissions
- Status history creation
- Transactional consistency
- Duplicate transition handling
- Dispute restoration
Security is a core concern because QuickServe handles authentication, customer information, payments, operational data, and uploaded media.
- Passwords are hashed with bcrypt.
- JWT secrets are stored in environment variables.
- Access and refresh tokens use separate secrets.
- Credentials are never committed to the repository.
Sensitive values such as:
- Price
- Payment state
- Order status
- User identity
- Role
must be determined or verified server-side.
- Verify Paystack webhook signatures.
- Verify transaction ownership.
- Verify expected payment amounts.
- Prevent duplicate webhook processing.
- Restrict file types.
- Restrict file sizes.
- Authenticate upload requests.
- Verify resource ownership.
Production deployments should use:
- HTTPS
- Explicit CORS configuration
- Rate limiting
- Secure secrets
- Structured logging
- Error monitoring
- Database backups
If you discover a security vulnerability, please avoid publishing exploit details in a public issue. Use the repository's private security reporting process when available.
QuickServe can be deployed using:
┌──────────────┐
│ Render │
│ Node API │
└──────┬───────┘
│
▼
┌──────────────┐
│ Neon │
│ PostgreSQL │
└──────────────┘
Run migrations against the production database:
DATABASE_URL="your-neon-connection-string" npx knex migrate:latestSeed required reference data:
DATABASE_URL="your-neon-connection-string" npx knex seed:runProduction environment variables should be configured through the hosting provider's secret/environment management system.
QuickServe is an actively developed backend project.
- Core authentication
- Order lifecycle
- Role-based access
- Rider workflow
- Partner workflow
- Payments
- Disputes
- Payouts
- Operational administration
- Redis-backed rate limiting
- Firebase push notifications
- Dedicated transactional email provider
- Platform commission model
- Payment reconciliation
- Refund workflow
- Advanced geospatial services
- OpenAPI documentation
- Automated API testing
- Structured logging and observability
- Expanded dispute workflows
- Rider availability and location tracking
- Partner capacity management
Contributions, suggestions, and improvements are welcome.
- Read the project architecture.
- Check existing issues before creating a duplicate.
- Keep changes focused.
- Follow the existing module structure.
- Avoid putting business logic in controllers.
- Add validation for externally supplied data.
- Add or update tests for important behavior.
- Keep secrets and credentials out of commits.
git checkout -b feature/your-featureMake your changes, test them locally, then commit:
git add .
git commit -m "feat: describe your change"Push the branch:
git push origin feature/your-featureThen open a pull request.
Where practical, use conventional commit prefixes:
feat: add partner order acceptance
fix: prevent duplicate payment processing
refactor: simplify order transition service
docs: update API documentation
test: cover cancellation transitions
chore: update dependencies
Detailed request and response documentation should live alongside the project as the API matures.
A future OpenAPI specification is planned so that the API can be consumed by:
- Frontend applications
- Mobile applications
- Internal operations dashboards
- Third-party integrations
- API testing tools
The long-term direction for QuickServe is to evolve from a backend learning project into a robust marketplace infrastructure.
- Customer application
- Rider application
- Partner dashboard
- Operations dashboard
- Dynamic service catalog
- Partner discovery
- Ratings and reviews
- Availability management
- Service-area expansion
- Rider location tracking
- Route optimization
- Delivery ETA
- Rider availability
- Pickup scheduling
- Platform commissions
- Refunds
- Payment reconciliation
- Automated partner settlements
- Financial reporting
- Redis
- Background jobs
- Queue-based notifications
- Observability
- Automated deployment pipelines
- Comprehensive API test suite
This project is licensed under the MIT License.
See the LICENSE file for the full license text.
QuickServe Backend
Built with Node.js, Express, PostgreSQL, and a focus on practical backend engineering.
QuickServe is more than a collection of API endpoints.
The architecture is designed around a few core principles:
The client is not the source of truth.
Business rules belong in services.
State changes must be explicit and validated.
Important operations should be auditable.
External integrations should be isolated behind clear boundaries.
Predictable APIs are easier to build, test, and maintain.
The project is intentionally structured to demonstrate how a real marketplace backend can be designed, developed, tested, and evolved over time.