Wooster Server is the backend service for the Wooster AI-powered trip planning application.
It is built using Express and TypeScript, it uses Drizzle as an ORM, and integrates with Google's Gemini API for AI services and Supabase for cloud database storage.
The backend manages trip and destination data, providing API routes for creating, managing, and retrieving trip itineraries and destination details.
This is the backend repository for the Wooster project. The frontend code can be found in the frontend repository.
The full application is deployed at www.trywooster.live
Here’s an overview of the database schema for Wooster, managing user trips, itineraries, activities, and destinations:
- Getting Started
- Prerequisites
- Environment Variables
- Installation
- API Routes
- Testing
- Utilities
- License
To get the backend server up and running, follow these steps:
- Google Gemini API Key: You will need to generate your own Google Gemini API key to use AI features.
- Supabase Database: Create a Supabase account and set up a database to manage your trip and destination data.
You will need to create a .env file at the root of the project with the following environment variables. Replace the placeholders with your actual credentials:
# .env
PORT=your_port_number
SUPABASE_URL=your_supabase_project_url
SUPABASE_KEY=your_supabase_api_key
DATABASE_URL=postgresql://<USER>:<PASSWORD>@<HOST>:<PORT>/<DATABASE>
GOOGLE_GEMINI_API_KEY=your_google_gemini_api_key
- Replace your_port_number, your_supabase_project_url, your_supabase_api_key, and your_google_gemini_api_key with your actual credentials.
- In the DATABASE_URL, Supabase provides all the details in your database settings except the password. You will need to replace the password manually when creating the .env file.
This project uses Drizzle ORM to manage the database schema. When setting up the project, Drizzle will handle creating and applying the schema to the database.
Steps to Initialize the Database
- Install Dependencies: Ensure that all dependencies are installed by running:
npm install
- Run Migrations: Drizzle will automatically create and apply the necessary database tables and schema defined in the project. To ensure the database is fully initialized or updated, run:
npm run drizzle:migrate
This will apply any pending migrations and ensure the database schema matches the project’s schema.
Notes:
- The schema will be created from scratch if no existing schema is found in the database.
- Drizzle will ensure that the database is kept up-to-date with the latest migrations whenever they are added to the project.
Make sure the .env file is properly configured (as detailed above) before running the migration command.
-
Clone the repository:
git clone https://github.com/your-username/wooster-server.git
-
Navigate to the project directory:
cd wooster-server
-
Install dependencies:
npm install
-
Start the server:
npm start
The server will start on the port specified in your .env
file.
Here are the available API routes in the application:
GET /trips
: Fetch all trips from the Supabase database. (Requires authentication)GET /trips/:id
: Fetch detailed information for a specific trip by its ID. (Requires authentication)POST /trips
: Save a new trip to the Supabase database. (Rate-limited, requires authentication)DELETE /trips/:tripId
: Delete a trip by its ID from the Supabase database. (Requires authentication)
GET /destinations/search
: Search for destinations based on query parameters.GET /destinations/destinationName
: Fetch detailed information about a specific destination by name.GET /destinations
: Fetch a list of all destinations. (Requires authentication)GET /destination/:destinationName/activities
: Retrieve a list of activities for a specific destination by name. (Requires authentication)POST /destination
: Add a new destination to the Supabase database. (Rate-limited, requires authentication)DELETE /destinations/:destinationId
: Remove a destination by its ID from the Supabase database.
GET /saved-destinations
: Retrieve a list of saved destinations for the current user. (Requires authentication)POST /saved-destinations/:destinationId
: Save a destination to the user's saved list by its ID. (Requires authentication)DELETE /saved-destinations/:destinationId
: Remove a saved destination by its ID for the current user. (Requires authentication)
This project includes both unit and integration tests using Jest and Supertest. The tests cover core application functionalities, including routes, services, and middleware behavior. Tests use mocks and stubs for dependencies to isolate components and validate error handling, input validation, and response formats.
- Unit Tests: Verify individual functions and modules, ensuring that they behave as expected in isolation. Mocks are used to simulate dependencies.
- Integration Tests: Validate the interaction between different parts of the system, primarily focusing on API routes and the responses returned by the application.
The tests use Jest to mock dependencies such as services and middleware, enabling isolated testing of specific components. For example, auth-middleware
and services like destination-service
and saved-destination-service
are mocked to simulate responses and control test outcomes. Supabase database operations are also mocked, allowing tests to run without altering live data.
To run all tests, use:
npm test
Below is a summary of an example test suite for POST /destinations in the Destination Routes. It includes various scenarios to validate route behavior under different conditions:
- Success Case: Verifies that a new destination is created and saved successfully when valid data is provided.
- Validation Error: Ensures that a 400 error is returned when required fields are missing.
- Service Error Handling: Tests how the application handles errors from external services, like the AI-based destination generation.
- Duplicate Handling: Ensures the system handles cases where a destination is already saved, returning a 409 conflict.
To maintain test isolation and avoid side effects, each test case wraps Supabase operations within transactions that are rolled back after each test. This helps simulate real scenarios without affecting the database.
Here is an example code snippet demonstrating a test case for creating a new destination in POST /destinations:
it('creates a new destination', async () => {
const createdDestination = { /* mock destination data */ };
const savedDestination = { /* mock saved data */ };
mockedFindDestinationByName.mockResolvedValue(null); // Simulate no pre-existing destination
mockedGenerateDestinationData.mockResolvedValue(JSON.stringify(mockDestinationData));
mockedAddDestination.mockResolvedValue(createdDestination);
mockedAddSavedDestination.mockResolvedValue(savedDestination);
const res = await request(app)
.post('/api/destinations')
.set('Authorization', mockAuthHeader)
.send({ destination: 'Paris' })
.expect(201);
expect(res.body.destination).toEqual(expectedResponse);
});
The cleanLLMJsonResponse utility function cleans the response from a language model by removing markdown code blocks, comments, and incomplete URLs. This ensures the result is a clean string for further parsing.
const cleanLLMJsonResponse = (text: string): string => {
// Step 1: Remove markdown code blocks with any language specification
const withoutCodeBlocks = text.replace(
/```(?:json)?\s*([\s\S]*?)\s*```/g,
'$1'
);
// Step 2: Remove potential comments
const withoutComments = withoutCodeBlocks.replace(
/\/\*[\s\S]*?\*\/|\/\/.*/g,
''
);
// Step 3: Detect and replace incomplete URLs by adding a placeholder if needed
const withCompleteUrls = withoutComments.replace(
/"website":\s*"https:([^",}]*)/g,
`"website": "https://example.com"`
);
// Step 4: Trim whitespace from start and end
return withCompleteUrls.trim();
};
This project is licensed under the MIT License. See the LICENSE file for details.