Deploy an Influencer Marketplace on Render
Build a full-stack React and Node.js influencer marketplace with a PostgreSQL backend.
Introduction
30 Second Summary
Think about scrolling through a marketplace app, browsing profiles, filtering by what you need, and picking the perfect match. That same experience powers how businesses find influencers to promote their products.
In this project, you will build a full-stack influencer discovery marketplace where businesses can browse, search, and filter influencer profiles. You will deploy it live on Render with a real database behind it.
What You'll Build
A full-stack influencer marketplace MVP with a React and Tailwind CSS frontend, a Node.js/Express REST API, and a PostgreSQL database, all deployed on Render.
By the end of this project, you'll have:
- A polished React frontend with a responsive card grid, search bar, and category filter buttons.
- A Node.js/Express REST API with endpoints for fetching and filtering influencer data from a PostgreSQL database.
- A live deployed app on Render accessible via a public URL.
- A Secret Mission to add sorting and a detail modal that makes the marketplace feel production-ready.
Are there any prerequisites?
You should have basic familiarity with HTML, CSS, and JavaScript. You will also need a GitHub account, a Render account, and Node.js and Git installed on your machine.
Before We Start
Before We Start...
Set Up the Project Structure
Every marketplace app needs two engines working together. A frontend that displays data beautifully and a backend that manages the data. In this project, you are building both from scratch using React for the frontend and Express for the backend.
The fastest way to keep these two pieces organized is a monorepo. One root folder holds separate client and server directories. In this step, you will create that folder structure, scaffold a React app with Vite, and spin up a basic Express server.
In this step, get ready to:
- Create the project folder structure with Git initialized.
- Scaffold the React frontend with Vite and Tailwind CSS.
- Set up the Express backend with a health check endpoint.
Create the monorepo folder structure
Your project needs a home on your computer. You will create a root folder with a server subdirectory and initialize Git for version control.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the project folder, move into it, set up the server directory, and initialize a Git repository by running these commands:
mkdir influencer-marketplace
cd influencer-marketplace
mkdir server
git init
- Confirm the folder structure by running:
ls
You should see server in the output. Vite will create the client folder for you in the next substep.
What is a monorepo?
A monorepo keeps your frontend and backend code in a single repository. This simplifies deployment because you can build and serve everything from one place.
Scaffold the React frontend with Vite
Vite is a build tool that scaffolds a ready-to-go React project in seconds. You will also install Tailwind CSS for styling.
- Scaffold a new React project in the client folder by running this command from your influencer-marketplace directory:
npm create vite@latest client -- --template react
Seeing a prompt to install create-vite?
If npm asks Need to install the following packages: create-vite@latest. Ok to proceed?, type y and press Enter.
- Move into the client folder and install all dependencies including Tailwind CSS by running these commands:
cd client
npm install
npm install tailwindcss @tailwindcss/vite
What are these packages?
- tailwindcss is the Tailwind CSS framework that lets you style elements with utility classes directly in your HTML.
- @tailwindcss/vite is the official Vite plugin that connects Tailwind to your build pipeline.
Install failing?
- Make sure you are inside the client folder before running npm install.
- Check that Node.js is version 20.19 or higher by running node --version. Vite 8 requires Node.js 20.19+ or 22.12+.
- Help me debug this npm install error.
Now open the project in VS Code so you can edit the configuration files.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Select the influencer-marketplace folder on your Desktop, then click Open.
You need to update two files to wire Tailwind into your Vite project.
- In VS Code's Explorer sidebar, expand the client folder and open vite.config.js.
- Replace the entire file with this code:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// Configure Vite with React and Tailwind CSS plugins
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})
What does this config do?
- The plugins array tells Vite which tools to use during development and builds.
- react() enables JSX and hot module replacement for React.
- tailwindcss() processes your Tailwind utility classes into real CSS.
- In the Explorer sidebar, expand client/src and open index.css.
- Delete all the existing content in the file.
- Replace it with this single line:
@import "tailwindcss";
Why replace the whole file?
Vite's scaffold includes demo styles you don't need. The single @import "tailwindcss" line loads Tailwind's full utility class library. In Tailwind CSS v4, this one import is all the configuration you need.
- Save both vite.config.js and index.css.
- Open a terminal in VS Code by pressing Ctrl+` (backtick key, located above Tab).
- Navigate to the client folder and start the Vite dev server by running these commands:
cd client
npm run dev
You should see a URL like http://localhost:5173 in the terminal output. Open that URL in your browser to confirm the default Vite + React page loads.
- Press Ctrl+C in the terminal to stop the dev server.
Dev server won't start?
- Make sure you are inside the client folder when running npm run dev.
- Check that vite.config.js has no typos. The tailwindcss() call must include parentheses.
- Help me debug my Vite dev server.
Awesome, I've got everything!
Double check you have saved both vite.config.js and index.css before moving on.
I'd like to double check the full code
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// Configure Vite with React and Tailwind CSS plugins
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})
@import "tailwindcss";
Initialize the Express backend
Express is a lightweight web framework for Node.js. It will serve your API endpoints and, in production, your React build too.
- Navigate to the server folder by running this command:
cd ../server
- Initialize a new Node.js project by running:
npm init -y
- Install Express, the PostgreSQL client, and the CORS middleware by running:
npm install express@5.2.1 pg@8.20.0 cors@2.8.5
What are these packages?
- express@5.2.1 is the web framework that handles API routes and serves files.
- pg@8.20.0 is the PostgreSQL client for Node.js. You will use it to run SQL queries against your database.
- cors@2.8.5 is middleware that allows the React dev server to make requests to this backend without being blocked by the browser.
Install command failing?
- Confirm you are inside the server folder by running pwd. The path should end in influencer-marketplace/server.
- If you see a network error, check your internet connection and try again.
- Help me fix this npm install error in my server folder.
Now create the main server file with a basic health check endpoint.
- In VS Code's Explorer sidebar, right-click on the server folder.
- Select New File and name it index.js.
- Copy the following code into server/index.js:
const express = require('express');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 3001;
// Enable CORS and JSON parsing
app.use(cors());
app.use(express.json());
// Health check endpoint
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
// Start the server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
What does this code do?
- express() creates a new Express application instance.
- cors() allows the React dev server to make requests to this backend without the browser blocking them.
- The /api/health route returns a simple JSON response to confirm the server is alive.
- process.env.PORT || 3001 uses the environment variable PORT if available (which Render sets automatically), or falls back to 3001 for local development.
- Save index.js.
- Make sure your VS Code terminal is in the server folder, then start the Express server by running:
node index.js
You should see Server running on port 3001 in the terminal.
- Open http://localhost:3001/api/health in your browser.
You should see {"status":"ok"} displayed on the page.
- Press Ctrl+C in the terminal to stop the server.
Server not starting?
- Run pwd to confirm you are in the server folder.
- If you see Error: Cannot find module 'express', run npm install again in the server folder to reinstall dependencies.
- If port 3001 is already in use, stop whatever process is using it or change the port number in index.js.
- Help me debug my Express server startup error.
Awesome, I've got everything!
Double check you have saved server/index.js before moving on.
I'd like to double check the full code
const express = require('express');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 3001;
// Enable CORS and JSON parsing
app.use(cors());
app.use(express.json());
// Health check endpoint
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
// Start the server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Your project skeleton is in place. The React frontend and Express backend are both running independently. Next up, you will build the API routes and connect them to a PostgreSQL database.
Build the Express API and Database Layer
Your project structure is in place with a React frontend and an Express server ready to go. Now it's time to give your API real data to serve.
In this step, you'll connect your Express server to a PostgreSQL database, fill it with sample influencer profiles, and build an API endpoint that supports filtering and search.
In this step, get ready to:
- Set up the PostgreSQL database connection and seed script.
- Build the API endpoint for fetching and filtering influencers.
- Test the full API locally with real data.
Set up the PostgreSQL connection and seed script
Your Express server needs a way to talk to a PostgreSQL database. The pg package (already installed in Step 1) provides a connection pool that manages database connections efficiently.
- Create a new file called db.js inside your server folder and paste the following code:
const { Pool } = require('pg');
// Create a connection pool using the DATABASE_URL environment variable
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
// Enable SSL when connecting to Render's hosted Postgres
ssl: process.env.DATABASE_URL && process.env.DATABASE_URL.includes('render.com')
? { rejectUnauthorized: false }
: false,
});
module.exports = pool;
What does this code do?
- Pool creates a pool of reusable database connections. Instead of opening a new connection every time someone hits your API, the pool shares connections across requests.
- connectionString reads the database URL from an environment variable. This keeps your credentials out of source code.
- The ssl setting automatically enables SSL when connecting to a Render-hosted database. Local databases skip SSL.
- Save db.js.
You won't see a change yet. The seed script you create next uses this connection.
Awesome, I've got everything!
Great. Double check you've saved db.js before moving on.
I'd like to double check the full code
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.DATABASE_URL && process.env.DATABASE_URL.includes('render.com')
? { rejectUnauthorized: false }
: false,
});
module.exports = pool;
Now you need a script that creates the database table and fills it with sample influencer data. This is called a seed script.
- Create a new file called seed.js inside your server folder and paste the following code:
const pool = require('./db');
const seedDatabase = async () => {
try {
// Drop and recreate the influencers table
await pool.query(`
DROP TABLE IF EXISTS influencers;
CREATE TABLE influencers (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
platform VARCHAR(50) NOT NULL,
niche VARCHAR(50) NOT NULL,
followers INTEGER NOT NULL,
rating NUMERIC(2,1) NOT NULL,
image_url VARCHAR(255),
bio TEXT
);
`);
// Sample influencer data across five categories
const influencers = [
{ name: 'Aria Chen', platform: 'Instagram', niche: 'Fashion', followers: 1200000, rating: 4.8, bio: 'Fashion designer and style influencer based in NYC. Known for sustainable fashion tips and outfit inspiration.' },
{ name: 'Marcus Johnson', platform: 'YouTube', niche: 'Tech', followers: 890000, rating: 4.6, bio: 'Tech reviewer covering the latest gadgets, smartphones, and software. Honest reviews you can trust.' },
{ name: 'Sofia Rodriguez', platform: 'TikTok', niche: 'Fitness', followers: 2100000, rating: 4.9, bio: 'Certified personal trainer sharing quick workout routines and nutrition advice for busy professionals.' },
{ name: 'Jake Williams', platform: 'Instagram', niche: 'Food', followers: 670000, rating: 4.3, bio: 'Home chef exploring street food and hidden restaurant gems around the world.' },
{ name: 'Luna Park', platform: 'YouTube', niche: 'Travel', followers: 1500000, rating: 4.7, bio: 'Solo travel vlogger documenting adventures across 60+ countries with budget-friendly tips.' },
{ name: 'Dev Patel', platform: 'TikTok', niche: 'Tech', followers: 950000, rating: 4.5, bio: 'Software engineer making coding accessible with fun 60-second tutorials and tech explainers.' },
{ name: 'Emma Laurent', platform: 'Instagram', niche: 'Fashion', followers: 3200000, rating: 4.9, bio: 'Parisian fashion blogger and brand consultant. Collaborating with luxury and indie brands alike.' },
{ name: 'Tyler Brooks', platform: 'YouTube', niche: 'Fitness', followers: 780000, rating: 4.4, bio: 'Strength coach and former athlete sharing workout plans and recovery science content.' },
{ name: 'Mia Tanaka', platform: 'TikTok', niche: 'Food', followers: 1800000, rating: 4.8, bio: 'Japanese-American food creator known for aesthetic recipe videos and fusion cooking experiments.' },
{ name: 'Carlos Rivera', platform: 'Instagram', niche: 'Travel', followers: 420000, rating: 4.2, bio: 'Adventure photographer capturing breathtaking landscapes and sharing travel photography tips.' },
{ name: 'Zara Ahmed', platform: 'YouTube', niche: 'Fashion', followers: 560000, rating: 4.1, bio: 'Modest fashion advocate creating inclusive style content for diverse audiences worldwide.' },
{ name: 'Ryan Kim', platform: 'TikTok', niche: 'Tech', followers: 1100000, rating: 4.6, bio: 'AI researcher breaking down complex technology topics into engaging short-form content.' },
];
// Insert each influencer with a generated avatar URL
for (const inf of influencers) {
await pool.query(
`INSERT INTO influencers (name, platform, niche, followers, rating, image_url, bio)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[
inf.name,
inf.platform,
inf.niche,
inf.followers,
inf.rating,
`https://ui-avatars.com/api/?name=${encodeURIComponent(inf.name)}&background=6366f1&color=fff&size=128`,
inf.bio,
]
);
}
console.log('Database seeded successfully!');
process.exit(0);
} catch (err) {
console.error('Error seeding database:', err);
process.exit(1);
}
};
seedDatabase();
What does this script do?
- DROP TABLE IF EXISTS removes any existing table so you can re-run the script cleanly. CREATE TABLE builds the influencers table with columns for each piece of profile data.
- The influencers array contains 12 sample profiles spread across Fashion, Tech, Fitness, Food, and Travel categories.
- Each insert uses parameterized values ($1, $2, etc.) instead of string concatenation. This prevents SQL injection attacks.
- The avatar URLs use the ui-avatars.com API, which generates letter-based profile images from each influencer's name. No sign-up required.
- Save seed.js.
You won't run this script yet. You'll connect to a database and seed it in substep 3.
Awesome, I've got everything!
Great. Double check you've saved seed.js before moving on.
I'd like to double check the full code
const pool = require('./db');
const seedDatabase = async () => {
try {
await pool.query(`
DROP TABLE IF EXISTS influencers;
CREATE TABLE influencers (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
platform VARCHAR(50) NOT NULL,
niche VARCHAR(50) NOT NULL,
followers INTEGER NOT NULL,
rating NUMERIC(2,1) NOT NULL,
image_url VARCHAR(255),
bio TEXT
);
`);
const influencers = [
{ name: 'Aria Chen', platform: 'Instagram', niche: 'Fashion', followers: 1200000, rating: 4.8, bio: 'Fashion designer and style influencer based in NYC. Known for sustainable fashion tips and outfit inspiration.' },
{ name: 'Marcus Johnson', platform: 'YouTube', niche: 'Tech', followers: 890000, rating: 4.6, bio: 'Tech reviewer covering the latest gadgets, smartphones, and software. Honest reviews you can trust.' },
{ name: 'Sofia Rodriguez', platform: 'TikTok', niche: 'Fitness', followers: 2100000, rating: 4.9, bio: 'Certified personal trainer sharing quick workout routines and nutrition advice for busy professionals.' },
{ name: 'Jake Williams', platform: 'Instagram', niche: 'Food', followers: 670000, rating: 4.3, bio: 'Home chef exploring street food and hidden restaurant gems around the world.' },
{ name: 'Luna Park', platform: 'YouTube', niche: 'Travel', followers: 1500000, rating: 4.7, bio: 'Solo travel vlogger documenting adventures across 60+ countries with budget-friendly tips.' },
{ name: 'Dev Patel', platform: 'TikTok', niche: 'Tech', followers: 950000, rating: 4.5, bio: 'Software engineer making coding accessible with fun 60-second tutorials and tech explainers.' },
{ name: 'Emma Laurent', platform: 'Instagram', niche: 'Fashion', followers: 3200000, rating: 4.9, bio: 'Parisian fashion blogger and brand consultant. Collaborating with luxury and indie brands alike.' },
{ name: 'Tyler Brooks', platform: 'YouTube', niche: 'Fitness', followers: 780000, rating: 4.4, bio: 'Strength coach and former athlete sharing workout plans and recovery science content.' },
{ name: 'Mia Tanaka', platform: 'TikTok', niche: 'Food', followers: 1800000, rating: 4.8, bio: 'Japanese-American food creator known for aesthetic recipe videos and fusion cooking experiments.' },
{ name: 'Carlos Rivera', platform: 'Instagram', niche: 'Travel', followers: 420000, rating: 4.2, bio: 'Adventure photographer capturing breathtaking landscapes and sharing travel photography tips.' },
{ name: 'Zara Ahmed', platform: 'YouTube', niche: 'Fashion', followers: 560000, rating: 4.1, bio: 'Modest fashion advocate creating inclusive style content for diverse audiences worldwide.' },
{ name: 'Ryan Kim', platform: 'TikTok', niche: 'Tech', followers: 1100000, rating: 4.6, bio: 'AI researcher breaking down complex technology topics into engaging short-form content.' },
];
for (const inf of influencers) {
await pool.query(
`INSERT INTO influencers (name, platform, niche, followers, rating, image_url, bio)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[
inf.name,
inf.platform,
inf.niche,
inf.followers,
inf.rating,
`https://ui-avatars.com/api/?name=${encodeURIComponent(inf.name)}&background=6366f1&color=fff&size=128`,
inf.bio,
]
);
}
console.log('Database seeded successfully!');
process.exit(0);
} catch (err) {
console.error('Error seeding database:', err);
process.exit(1);
}
};
seedDatabase();
Create the API routes
Your database layer is ready. Now you need an API endpoint that queries the database and returns influencer data as JSON. The endpoint also needs to support filtering by category and searching by name.
- In server/index.js, find the line that says const cors = require('cors'); and add this import below it:
const pool = require('./db');
- Next, find the health check route (/api/health) and copy the following endpoint below it:
// Get influencers with optional filtering and sorting
app.get('/api/influencers', async (req, res) => {
try {
const { niche, search, sort } = req.query;
let query = 'SELECT * FROM influencers WHERE 1=1';
const params = [];
// Filter by category if provided
if (niche && niche !== 'All') {
params.push(niche);
query += ` AND niche = $${params.length}`;
}
// Search by name (case-insensitive)
if (search) {
params.push(`%${search}%`);
query += ` AND name ILIKE $${params.length}`;
}
// Sort results based on query parameter
if (sort === 'rating') {
query += ' ORDER BY rating DESC';
} else if (sort === 'followers') {
query += ' ORDER BY followers DESC';
} else if (sort === 'followers_asc') {
query += ' ORDER BY followers ASC';
} else {
query += ' ORDER BY id ASC';
}
const result = await pool.query(query, params);
res.json(result.rows);
} catch (err) {
console.error('Error fetching influencers:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
What does this endpoint do?
- WHERE 1=1 is a SQL trick that makes it easy to chain optional filters with AND. Without it, you'd need complex logic to decide whether to start with WHERE or AND.
- $${params.length} creates parameterized queries. Instead of inserting user input directly into the SQL string (which would allow SQL injection), the database engine treats each parameter as a safe value.
- ILIKE is PostgreSQL's case-insensitive version of LIKE. Searching for "aria" matches "Aria Chen".
- The sort parameter supports three options: rating, followers, and followers_asc. The default sorts by id (insertion order).
What is SQL injection?
SQL injection is an attack where someone sends malicious SQL code through an input field. If your code pastes user input directly into a query string, the attacker can delete tables or steal data. Parameterized queries prevent this by keeping user input separate from the SQL logic.
- Save index.js.
You won't see a change yet. You'll test this endpoint after connecting to a database in the next substep.
Seeing a syntax error in your editor?
- Check that the backtick characters (`) in the template literals are backticks, not single quotes. They're on the key to the left of 1 on your keyboard.
- Make sure every opening brace { has a matching closing brace }. The endpoint has nested blocks (try/catch inside the route handler).
- Make sure you pasted the endpoint below the /api/health route but above app.listen().
Why is my influencers endpoint showing a syntax error?
Awesome, I've got everything!
Great. Double check you've saved index.js before moving on.
I'd like to double check the full code
const express = require('express');
const cors = require('cors');
const pool = require('./db');
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
// Health check
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
// Get influencers with optional filtering and sorting
app.get('/api/influencers', async (req, res) => {
try {
const { niche, search, sort } = req.query;
let query = 'SELECT * FROM influencers WHERE 1=1';
const params = [];
if (niche && niche !== 'All') {
params.push(niche);
query += ` AND niche = $${params.length}`;
}
if (search) {
params.push(`%${search}%`);
query += ` AND name ILIKE $${params.length}`;
}
if (sort === 'rating') {
query += ' ORDER BY rating DESC';
} else if (sort === 'followers') {
query += ' ORDER BY followers DESC';
} else if (sort === 'followers_asc') {
query += ' ORDER BY followers ASC';
} else {
query += ' ORDER BY id ASC';
}
const result = await pool.query(query, params);
res.json(result.rows);
} catch (err) {
console.error('Error fetching influencers:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Test the API locally
Your API code is written, but it needs a real database to connect to. You need a PostgreSQL database with a connection URL that your server can use.
I have PostgreSQL installed locally
- Create a database for this project by running:
createdb influencer_marketplace
Your connection URL is typically postgresql://localhost:5432/influencer_marketplace. If your PostgreSQL setup uses a username and password, include them in the URL.
I need a database
You can create a free PostgreSQL database on Render in under a minute. You'll reuse this same database when you deploy later.
- Go to dashboard.render.com and sign in.
- Click New in the top navigation bar.
- Select PostgreSQL.
- Enter influencer-db as the name.
- Choose the Free instance type.
- Click Create Database.
- On the database info page, scroll down to the Connections section.
- Copy the External Database URL. This is what you'll use to connect from your local machine.
Why External and not Internal?
Render provides two database URLs. The Internal URL only works between Render services. The External URL works from anywhere, including your local machine. You'll switch to the Internal URL when you deploy in Step 5.
Now set the connection URL as an environment variable so your server can find it.
- In your terminal, make sure you're inside the server folder.
- Set the DATABASE_URL environment variable by running this command:
macOS
export DATABASE_URL="your-database-connection-url"
Windows
$env:DATABASE_URL="your-database-connection-url"
- Replace your-database-connection-url with the connection URL you copied (e.g. postgresql://localhost:5432/influencer_marketplace for local PostgreSQL, or the Render External Database URL).
Pro tip: keep the terminal open
Environment variables set with export or $env: only last for the current terminal session. If you close the terminal and reopen it, you'll need to set DATABASE_URL again.
- Seed the database with sample data by running:
node seed.js
You should see Database seeded successfully! in the terminal.
Seeing a connection error?
- If you see ECONNREFUSED, your DATABASE_URL is not set correctly or your local PostgreSQL service is not running.
- If you see password authentication failed, double check that the username and password in your connection URL are correct.
- If you see an SSL error when connecting to Render, make sure your db.js file includes the ssl configuration shown above.
Help me debug my database connection error
- Start the Express server by running:
node index.js
You should see Server running on port 3001 in the terminal.
- Open your browser and navigate to http://localhost:3001/api/influencers.
You should see a JSON array containing 12 influencer objects, each with fields like name, platform, niche, followers, and rating.
- Test the filtering by navigating to http://localhost:3001/api/influencers?niche=Tech.
You should see only the three Tech influencers (Marcus Johnson, Dev Patel, and Ryan Kim).
- Test the search by navigating to http://localhost:3001/api/influencers?search=aria.
You should see only Aria Chen. The search is case-insensitive, so "aria" matches "Aria".
Seeing an empty array or an error?
- An empty array ([]) means the seed script did not run successfully. Stop the server with Ctrl+C, run node seed.js again, and restart with node index.js.
- If you see Internal server error, check your terminal for the full error message. It usually means the database connection is not working.
- Make sure you set DATABASE_URL in the same terminal session where you're running the server.
My /api/influencers endpoint returns an empty array or error
- Once you've confirmed the data loads correctly, stop the server by pressing Ctrl+C in the terminal.
Your API is serving real influencer data from a PostgreSQL database. Next up, you'll build the React frontend that turns this raw JSON into a polished card grid with search and filtering.
Build the React Frontend Components
Your Express API is running and your database is loaded with 12 influencer profiles. Now it is time to build the frontend that turns that raw data into something users can actually browse.
In this step, you will create React components for influencer cards, a search bar, and category filter buttons. Each component gets styled with Tailwind CSS for a polished, professional look.
In this step, get ready to:
- Create the InfluencerCard component with formatted stats and star ratings.
- Build SearchBar and CategoryFilter components for browsing and filtering.
- Wire up the App component to render the full marketplace layout.
Create the InfluencerCard component
React apps are built from reusable components. Each component is a self-contained piece of UI that receives data and renders it.
The first component you will build is InfluencerCard. It takes in an influencer object and renders a clean card showing their avatar, name, platform badge, niche tag, follower count, and star rating.
- Create a components folder inside client/src/ by running this command from your influencer-marketplace project root:
mkdir -p client/src/components
- Confirm the folder was created by running:
ls client/src/
- You should see components listed alongside the existing files like App.jsx and main.jsx.
- In VS Code, open the Explorer sidebar.
- Right-click on the client/src/components folder and select New File.
- Name the file InfluencerCard.jsx and paste the following code into it:
function InfluencerCard({ influencer }) {
// Format large follower counts into readable K/M suffixes
const formatFollowers = (count) => {
if (count >= 1000000) return (count / 1000000).toFixed(1) + 'M'
if (count >= 1000) return (count / 1000).toFixed(0) + 'K'
return count.toString()
}
// Render 5 stars, filled or empty based on the rating
const renderStars = (rating) => {
const stars = []
for (let i = 1; i <= 5; i++) {
stars.push(
<span key={i} className={i <= Math.round(rating) ? 'text-yellow-400' : 'text-gray-300'}>
★
</span>
)
}
return stars
}
// Map each platform to its brand color
const platformColors = {
Instagram: 'bg-pink-100 text-pink-700',
YouTube: 'bg-red-100 text-red-700',
TikTok: 'bg-gray-900 text-white',
}
return (
<div className="bg-white rounded-xl shadow-md hover:shadow-xl transition-all duration-300 hover:-translate-y-1 overflow-hidden cursor-pointer">
<div className="p-6">
{/* Avatar and name section */}
<div className="flex items-center gap-4 mb-4">
<img
src={influencer.image_url}
alt={influencer.name}
className="w-14 h-14 rounded-full ring-2 ring-indigo-100"
/>
<div>
<h3 className="font-semibold text-gray-900 text-lg">{influencer.name}</h3>
<span className={`inline-block px-2 py-0.5 rounded-full text-xs font-medium ${platformColors[influencer.platform] || 'bg-gray-100 text-gray-700'}`}>
{influencer.platform}
</span>
</div>
</div>
{/* Niche tag and follower count */}
<div className="flex items-center justify-between mb-3">
<span className="inline-block bg-indigo-50 text-indigo-700 px-3 py-1 rounded-full text-sm font-medium">
{influencer.niche}
</span>
<span className="text-gray-500 text-sm font-medium">
{formatFollowers(influencer.followers)} followers
</span>
</div>
{/* Star rating display */}
<div className="flex items-center gap-1">
{renderStars(influencer.rating)}
<span className="text-gray-500 text-sm ml-1">({influencer.rating})</span>
</div>
</div>
</div>
)
}
export default InfluencerCard
What does this code do?
- formatFollowers converts raw numbers like 1200000 into human-readable strings like 1.2M.
- renderStars loops from 1 to 5 and renders a filled yellow star or an empty gray star based on the influencer's rating.
- platformColors maps each social media platform to a colored pill style. Instagram gets pink, YouTube gets red, and TikTok gets dark.
- The Tailwind classes on the outer div give the card rounded corners, a shadow, and a subtle lift-on-hover effect.
- Save InfluencerCard.jsx.
You will not see a change yet. This component renders inside the App component you will build in substep 3.
Seeing a red underline in VS Code?
- Make sure the file is named InfluencerCard.jsx with the .jsx extension, not .js. The JSX syntax requires the .jsx extension.
- Check that the file is inside client/src/components/, not directly in client/src/.
- Verify that every opening curly brace { has a matching closing brace }. Missing braces are the most common syntax error in JSX.
Why is my InfluencerCard component showing a syntax error?
Awesome, I've got everything!
Double check you have saved InfluencerCard.jsx before moving on.
I'd like to double check the full code
function InfluencerCard({ influencer }) {
const formatFollowers = (count) => {
if (count >= 1000000) return (count / 1000000).toFixed(1) + 'M'
if (count >= 1000) return (count / 1000).toFixed(0) + 'K'
return count.toString()
}
const renderStars = (rating) => {
const stars = []
for (let i = 1; i <= 5; i++) {
stars.push(
<span key={i} className={i <= Math.round(rating) ? 'text-yellow-400' : 'text-gray-300'}>
★
</span>
)
}
return stars
}
const platformColors = {
Instagram: 'bg-pink-100 text-pink-700',
YouTube: 'bg-red-100 text-red-700',
TikTok: 'bg-gray-900 text-white',
}
return (
<div className="bg-white rounded-xl shadow-md hover:shadow-xl transition-all duration-300 hover:-translate-y-1 overflow-hidden cursor-pointer">
<div className="p-6">
<div className="flex items-center gap-4 mb-4">
<img
src={influencer.image_url}
alt={influencer.name}
className="w-14 h-14 rounded-full ring-2 ring-indigo-100"
/>
<div>
<h3 className="font-semibold text-gray-900 text-lg">{influencer.name}</h3>
<span className={`inline-block px-2 py-0.5 rounded-full text-xs font-medium ${platformColors[influencer.platform] || 'bg-gray-100 text-gray-700'}`}>
{influencer.platform}
</span>
</div>
</div>
<div className="flex items-center justify-between mb-3">
<span className="inline-block bg-indigo-50 text-indigo-700 px-3 py-1 rounded-full text-sm font-medium">
{influencer.niche}
</span>
<span className="text-gray-500 text-sm font-medium">
{formatFollowers(influencer.followers)} followers
</span>
</div>
<div className="flex items-center gap-1">
{renderStars(influencer.rating)}
<span className="text-gray-500 text-sm ml-1">({influencer.rating})</span>
</div>
</div>
</div>
)
}
export default InfluencerCard
Create the SearchBar and CategoryFilter components
Users need a way to find specific influencers. You will build two components for this: a search bar for typing a name, and a row of category filter buttons for browsing by niche.
- Right-click on the client/src/components folder in VS Code and select New File.
- Name the file SearchBar.jsx and paste the following code into it:
function SearchBar({ value, onChange }) {
return (
<div className="relative flex-1">
{/* Search icon positioned inside the input */}
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400">
🔍
</span>
<input
type="text"
placeholder="Search influencers by name..."
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all bg-white"
/>
</div>
)
}
export default SearchBar
What does this code do?
- The SearchBar receives its current value and an onChange callback from its parent component. This keeps the parent in control of the search state.
- The focus:ring-2 focus:ring-indigo-500 classes add a purple focus ring when the user clicks into the input. This gives clear visual feedback.
- Save SearchBar.jsx.
You will not see a change yet. The search bar will appear once the App component renders it in substep 3.
Awesome, I've got everything!
Make sure SearchBar.jsx is saved before continuing.
I'd like to double check the full code
function SearchBar({ value, onChange }) {
return (
<div className="relative flex-1">
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400">
🔍
</span>
<input
type="text"
placeholder="Search influencers by name..."
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all bg-white"
/>
</div>
)
}
export default SearchBar
Now for the category filter. This component renders a row of pill-shaped buttons that highlight when selected.
- Create another new file in client/src/components named CategoryFilter.jsx and paste the following code:
// The list of niche categories available for filtering
const categories = ['All', 'Fashion', 'Tech', 'Fitness', 'Food', 'Travel']
function CategoryFilter({ selected, onSelect }) {
return (
<div className="flex flex-wrap gap-2 mb-8">
{categories.map((cat) => (
<button
key={cat}
onClick={() => onSelect(cat)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
selected === cat
? 'bg-indigo-600 text-white shadow-md'
: 'bg-white text-gray-600 border border-gray-300 hover:border-indigo-400 hover:text-indigo-600'
}`}
>
{cat}
</button>
))}
</div>
)
}
export default CategoryFilter
What does this code do?
- The categories array defines the filter options. The component maps over this array to render one button per category.
- The ternary inside className checks if each button is the currently selected category. Selected buttons get a filled indigo background. Unselected buttons get a white background with a border.
- Save CategoryFilter.jsx.
You will not see a change yet. The next substep connects all three components together in the App.
Worried about a typo in one of the components?
Look at the bottom-left of VS Code. If you see a number next to a warning or error icon, click it to jump to the problem. Common issues are missing closing parentheses or mismatched curly braces in the className template literals.
Why is VS Code showing errors in my component files?
Awesome, I've got everything!
Make sure CategoryFilter.jsx is saved before continuing.
I'd like to double check the full code
const categories = ['All', 'Fashion', 'Tech', 'Fitness', 'Food', 'Travel']
function CategoryFilter({ selected, onSelect }) {
return (
<div className="flex flex-wrap gap-2 mb-8">
{categories.map((cat) => (
<button
key={cat}
onClick={() => onSelect(cat)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
selected === cat
? 'bg-indigo-600 text-white shadow-md'
: 'bg-white text-gray-600 border border-gray-300 hover:border-indigo-400 hover:text-indigo-600'
}`}
>
{cat}
</button>
))}
</div>
)
}
export default CategoryFilter
Wire up the App component
The App component is the main entry point of your React application. It manages the search and filter state, fetches influencer data from the API, and renders all three components you just built.
Vite scaffolded a default App.jsx with a counter demo. You will replace that entire file with your marketplace layout.
- Open client/src/App.jsx in VS Code.
- Select all the existing content with Cmd+A (macOS) or Ctrl+A (Windows) and delete it.
- Paste the following code to replace the entire file:
import { useState, useEffect } from 'react'
import InfluencerCard from './components/InfluencerCard'
import SearchBar from './components/SearchBar'
import CategoryFilter from './components/CategoryFilter'
function App() {
// State for the list of influencers, search text, selected category, and loading status
const [influencers, setInfluencers] = useState([])
const [search, setSearch] = useState('')
const [category, setCategory] = useState('All')
const [loading, setLoading] = useState(true)
// Fetch influencers from the API whenever search or category changes
useEffect(() => {
const fetchInfluencers = async () => {
setLoading(true)
try {
// Build the query string from current filter values
const params = new URLSearchParams()
if (category !== 'All') params.append('niche', category)
if (search) params.append('search', search)
const response = await fetch(`/api/influencers?${params.toString()}`)
const data = await response.json()
setInfluencers(data)
} catch (err) {
console.error('Error fetching influencers:', err)
} finally {
setLoading(false)
}
}
// Wait 300ms after the user stops typing before fetching
const debounceTimer = setTimeout(fetchInfluencers, 300)
return () => clearTimeout(debounceTimer)
}, [search, category])
return (
<div className="min-h-screen bg-gray-50">
{/* Page header */}
<header className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-gray-900">
🔍 Influencer Marketplace
</h1>
<p className="mt-1 text-gray-500">
Discover top influencers for your next campaign
</p>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
{/* Search bar */}
<div className="flex flex-col sm:flex-row gap-4 mb-8">
<SearchBar value={search} onChange={setSearch} />
</div>
{/* Category filter buttons */}
<CategoryFilter selected={category} onSelect={setCategory} />
{/* Conditional rendering: loading spinner, empty state, or influencer grid */}
{loading ? (
<div className="text-center py-12">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-indigo-500 border-r-transparent"></div>
<p className="mt-2 text-gray-500">Loading influencers...</p>
</div>
) : influencers.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-500 text-lg">No influencers found matching your criteria.</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{influencers.map((influencer) => (
<InfluencerCard key={influencer.id} influencer={influencer} />
))}
</div>
)}
</main>
</div>
)
}
export default App
What does this code do?
- Four useState hooks track the influencer list, the search term, the selected category, and whether data is loading.
- useEffect runs whenever search or category changes. It builds a query string from the current filter values and fetches matching influencers from your API.
- The 300ms setTimeout acts as a debounce. It waits for the user to stop typing before making the API call. This prevents firing a request on every keystroke.
- The JSX uses conditional rendering to show three possible states: a loading spinner, an empty results message, or the grid of influencer cards.
- The grid classes create a responsive layout. One column on mobile, two on small screens, three on medium, and four on large screens.
- Save App.jsx.
Time to see your work in the browser. You also need to clean up the default Vite CSS file so it does not interfere with your Tailwind styles.
- Delete the file client/src/App.css by right-clicking it in the VS Code Explorer sidebar and selecting Delete.
- Start the React dev server by running this command from the client folder:
cd client
npm run dev
- Open the URL shown in the terminal output (usually http://localhost:5173) in your browser.
You should see the marketplace header with "Influencer Marketplace" as the title. Below it, you should see the search bar input and six category filter buttons (All, Fashion, Tech, Fitness, Food, Travel).
The card grid shows "No influencers found matching your criteria." because the frontend is not connected to the backend API yet. That is expected. You will connect them in the next step.
Seeing the old Vite counter page instead?
- Make sure you saved App.jsx after replacing its content. Unsaved changes will not appear in the browser.
- Check that you deleted all the default Vite code from App.jsx before pasting the new code. Any leftover default code can cause errors.
- If the browser shows a blank white page, open DevTools with F12 and check the Console tab for error messages. A common error is a mismatched import path like ./components/influencerCard instead of ./components/InfluencerCard. File names are case-sensitive.
My React marketplace page is blank or showing errors in the console. Help me debug.
Awesome, I've got everything!
Confirm all four files are saved: InfluencerCard.jsx, SearchBar.jsx, CategoryFilter.jsx, and App.jsx.
I'd like to double check the full code
import { useState, useEffect } from 'react'
import InfluencerCard from './components/InfluencerCard'
import SearchBar from './components/SearchBar'
import CategoryFilter from './components/CategoryFilter'
function App() {
const [influencers, setInfluencers] = useState([])
const [search, setSearch] = useState('')
const [category, setCategory] = useState('All')
const [loading, setLoading] = useState(true)
useEffect(() => {
const fetchInfluencers = async () => {
setLoading(true)
try {
const params = new URLSearchParams()
if (category !== 'All') params.append('niche', category)
if (search) params.append('search', search)
const response = await fetch(`/api/influencers?${params.toString()}`)
const data = await response.json()
setInfluencers(data)
} catch (err) {
console.error('Error fetching influencers:', err)
} finally {
setLoading(false)
}
}
const debounceTimer = setTimeout(fetchInfluencers, 300)
return () => clearTimeout(debounceTimer)
}, [search, category])
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-gray-900">
🔍 Influencer Marketplace
</h1>
<p className="mt-1 text-gray-500">
Discover top influencers for your next campaign
</p>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
<div className="flex flex-col sm:flex-row gap-4 mb-8">
<SearchBar value={search} onChange={setSearch} />
</div>
<CategoryFilter selected={category} onSelect={setCategory} />
{loading ? (
<div className="text-center py-12">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-indigo-500 border-r-transparent"></div>
<p className="mt-2 text-gray-500">Loading influencers...</p>
</div>
) : influencers.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-500 text-lg">No influencers found matching your criteria.</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{influencers.map((influencer) => (
<InfluencerCard key={influencer.id} influencer={influencer} />
))}
</div>
)}
</main>
</div>
)
}
export default App
Your marketplace UI is taking shape. The header, search bar, and filter buttons are all rendering with clean Tailwind styles. Next up, you will connect this frontend to your Express API so real influencer data fills those cards.
Connect Frontend to Backend Locally
Your React components can render influencer cards, and your Express API can query the database and return results. The two pieces run on separate ports and don't know about each other yet.
In this step, you'll wire them together. A Vite proxy will route API requests to Express during development. Then you'll prepare Express to serve the React build for production, so a single server handles everything.
In this step, get ready to:
- Configure a Vite proxy to forward API requests to Express during development.
- Update Express to serve the built React files in production.
- Test the full stack locally to confirm the marketplace works end to end.
Configure the Vite proxy for local development
React's Vite dev server runs on port 5173. Express runs on port 3001. When App.jsx fetches /api/influencers, the request goes to port 5173, where no API exists.
A Vite proxy intercepts any request starting with /api and forwards it to Express on port 3001. The browser never knows the difference.
- Open client/vite.config.js and replace its contents with the following updated configuration:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
// Forward /api requests to Express during development
server: {
proxy: {
'/api': 'http://localhost:3001',
},
},
})
What does the server.proxy block do?
- The server.proxy setting tells Vite's dev server to intercept any request whose path starts with /api.
- Instead of returning a 404, Vite silently forwards those requests to http://localhost:3001, where your Express server is listening.
- This avoids CORS issues because the browser thinks the API lives on the same origin as the frontend.
- Save vite.config.js.
You won't see a change yet. The proxy takes effect when you restart the Vite dev server in the testing substep.
Update Express to serve the React build in production
In production on Render, there is no Vite dev server. Express needs to serve both the API and the built React files from a single process. You'll add three things to index.js to make this work.
- In server/index.js, find the line const express = require('express'); at the top of the file.
- Add this import directly below it:
const path = require('path');
- Now find the line app.use(express.json());. Add these lines below it:
// Serve static files from React build in production
app.use(express.static(path.join(__dirname, '../client/dist')));
- Scroll to the bottom of the file. Find the line app.listen(PORT, () => {. Add this catch-all route directly above it:
// Catch-all: serve React app for non-API routes
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../client/dist/index.html'));
});
What do these three additions do?
- path is a built-in Node.js module for building file paths that work across operating systems.
- express.static() tells Express to serve the compiled React files (HTML, CSS, JS) from the client/dist folder. This is the production build that Vite creates when you run npm run build.
- The catch-all route sends index.html for any URL that doesn't match an /api route. This enables client-side routing so React handles page navigation.
Why does the catch-all go last?
Express matches routes in the order they are defined. The app.get('*') route matches everything. If it came before the API routes, it would intercept /api/influencers and return the HTML page instead of JSON data. Placing it last ensures API routes get matched first.
- Save server/index.js.
You won't see a change yet. These additions only matter when Express serves a production build.
Awesome, I've got everything!
Great. Double check you've saved server/index.js before moving on.
I'd like to double check the full code
Here is the complete server/index.js with all the changes from this step included.
const express = require('express');
const path = require('path');
const cors = require('cors');
const pool = require('./db');
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
// Serve static files from React build in production
app.use(express.static(path.join(__dirname, '../client/dist')));
// Health check
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
// Get influencers with optional filtering and sorting
app.get('/api/influencers', async (req, res) => {
try {
const { niche, search, sort } = req.query;
let query = 'SELECT * FROM influencers WHERE 1=1';
const params = [];
if (niche && niche !== 'All') {
params.push(niche);
query += ` AND niche = $${params.length}`;
}
if (search) {
params.push(`%${search}%`);
query += ` AND name ILIKE $${params.length}`;
}
if (sort === 'rating') {
query += ' ORDER BY rating DESC';
} else if (sort === 'followers') {
query += ' ORDER BY followers DESC';
} else if (sort === 'followers_asc') {
query += ' ORDER BY followers ASC';
} else {
query += ' ORDER BY id ASC';
}
const result = await pool.query(query, params);
res.json(result.rows);
} catch (err) {
console.error('Error fetching influencers:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// Catch-all: serve React app for non-API routes
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../client/dist/index.html'));
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Catch-all route in the wrong place?
Make sure the app.get('*') catch-all route is the very last route defined, right above app.listen. If it appears before your /api routes, it will swallow those requests and return HTML instead of JSON.
help me check my route order in index.js
Test the full stack locally
You need two terminal windows running at the same time. One runs the Express API, the other runs the Vite dev server.
- Open a terminal window. In VS Code, press Ctrl+` (backtick) to open the integrated terminal.
- Navigate to the server folder and start the Express server by running these commands:
macOS
cd server
DATABASE_URL=your-database-connection-string node index.js
Windows
cd server
$env:DATABASE_URL="your-database-connection-string"
node index.js
What connection string do I use?
Use the same DATABASE_URL you used in Step 2 when you seeded the database and tested the API. Replace your-database-connection-string with that same value.
You should see Server running on port 3001 in the terminal output.
- Open a second terminal. In VS Code, click the + icon in the terminal panel to create a new terminal tab.
- Navigate to the client folder and start the Vite dev server by running these commands:
cd client
npm run dev
Vite prints a local URL in the terminal, usually http://localhost:5173.
- Open that URL in your browser.
You should see the full influencer marketplace. A grid of influencer cards loads with names, platform badges, niche tags, follower counts, and star ratings.
- Type a name in the search bar, like Aria. The grid should filter down to matching cards after a brief delay.
- Clear the search bar and click a category button like Tech. Only tech influencers should appear.
- Click All to confirm all 12 influencer cards return.
Cards not loading or seeing a blank page?
- Check that both terminals are running. The Express server should show Server running on port 3001 and the Vite server should show a local URL.
- Open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows). Look for network errors pointing to /api/influencers. A 500 error means the database connection failed. A 404 means the proxy is not forwarding requests.
- If you see a proxy error, make sure the server.proxy block in vite.config.js points to http://localhost:3001 and that you restarted the Vite dev server after saving the config change.
- Verify your DATABASE_URL is set correctly. A typo in the connection string causes Express to fail silently.
help me debug why my influencer cards aren't loading
Your marketplace runs perfectly on your machine. Next, you'll push it to GitHub and deploy it on Render so the whole world can see it.
Deploy to Render
Your marketplace app works perfectly on your local machine. The search bar filters influencers by name, the category buttons narrow results, and the card grid looks polished.
In this step, you will deploy everything to Render so anyone with an internet connection can use your marketplace. Render will host both your Express server and your PostgreSQL database on its free tier.
In this step, get ready to:
- Create a free PostgreSQL database on Render.
- Push your code to GitHub and deploy a Render Web Service.
- Seed the database and verify the live marketplace.
Create a PostgreSQL database on Render
Your marketplace needs a database that lives in the cloud. Render provides a free PostgreSQL tier that works well for MVPs and learning projects.
- Open your browser and go to dashboard.render.com.
- Log in to your Render account.
- Click + New in the top-right corner of the dashboard.
- Select Postgres from the dropdown menu.
- Enter influencer-db in the Name field.
- Leave the Database and User fields empty so Render generates them automatically.
- Choose a Region close to you. Remember your choice because the web service needs to be in the same region.
- Scroll down to Instance Type and select Free.
- Click Create Database.
Wait for the database status to show Available. This takes about 30 seconds.
- Click the Connect button in the top-right corner of the database page.
- Find the Internal Database URL field.
- Click the copy icon next to it and paste this URL somewhere safe. You will need it when setting up the web service.
What is an Internal Database URL?
This is a connection string that contains everything your server needs to talk to the database: the hostname, port, database name, username, and password. "Internal" means it only works for Render services in the same region. This gives lower latency and no extra cost.
When does the free database expire?
The free PostgreSQL database expires 30 days after creation. For a learning project, this is plenty of time. If you want to keep your data longer, you can upgrade to a paid instance later.
Push to GitHub and create a Render Web Service
Render deploys your code by pulling it from a GitHub repository. You need to push your project to GitHub first, then connect it to a Render Web Service.
Before pushing, you need a .gitignore file to keep dependency folders out of your repository.
- In VS Code, create a new file called .gitignore in the influencer-marketplace folder.
- Add the following content to the file:
node_modules
dist
.env
What does .gitignore do?
Git ignores any files or folders listed in this file. The node_modules folders contain thousands of dependency files that GitHub does not need. The dist folder contains the React build output that Render generates during deployment.
- Save .gitignore.
- Open your terminal in the influencer-marketplace folder.
- Stage all your files by running:
git add .
- Commit your code by running:
git commit -m "Initial commit"
Now create a repository on GitHub to push your code to.
- Open github.com in your browser.
- Click the + icon in the top-right corner.
- Select New repository.
- Enter influencer-marketplace as the repository name.
- Leave it set to Public.
- Do NOT check any of the initialization options (README, .gitignore, or license).
- Click Create repository.
- Copy the repository URL from the quick setup page. It looks like https://github.com/your-username/influencer-marketplace.git.
- Back in your terminal, add the remote and push your code by running:
git remote add origin [[YOUR_REPO_URL="https://github.com/your-username/influencer-marketplace.git"]]
git push -u origin main
Seeing "src refspec main does not match"?
Your default branch might be named master instead of main. Try running git push -u origin master instead.
If you see an authentication error, make sure you are logged in to GitHub in your terminal. GitHub requires a personal access token or SSH key for pushing.
Help me troubleshoot my git push
Your code is on GitHub. Now create a Render Web Service to deploy it.
- Go back to the Render Dashboard at dashboard.render.com.
- Click + New in the top-right corner.
- Select Web Service.
- Select Git Provider and connect your GitHub account if you have not done so already. Render will redirect you to GitHub to authorize access, then redirect you back.
- Find your influencer-marketplace repository in the list and click Connect.
Now fill in the service configuration fields.
- Enter influencer-marketplace in the Name field.
- Select the same Region you chose for your database.
- Set the Root Directory to server.
- Set the Build Command to:
cd ../client && npm install && npm run build && cd ../server && npm install && node seed.js
- Set the Start Command to node index.js.
- Scroll down to Instance Type and select Free.
Why set Root Directory to server?
The Root Directory tells Render where to run the Start Command from. Setting it to server means node index.js runs inside the server folder, which is where your Express app lives.
The Build Command uses cd ../client to reach the React code first, builds it, then comes back to install server dependencies. The final node seed.js populates the database with sample influencer data.
Now add the environment variable that connects your server to the database.
- Scroll down to the Environment Variables section.
- Click Add Environment Variable.
- Enter DATABASE_URL in the Key field.
- Paste the Internal Database URL you copied earlier into the Value field.
- Click Create Web Service.
Render starts the first deployment automatically. The build takes 2-5 minutes. You can watch the progress in the Events tab on the service page.
Build failing?
Check the Logs tab on your service page for error messages. Common issues:
- If you see cd: ../client: No such file or directory, make sure the Root Directory is set to server (not blank or the root of the repo).
- If you see MODULE_NOT_FOUND errors, make sure all your package.json files are committed to GitHub.
- If the seed script fails with a database connection error, double check that the DATABASE_URL environment variable is set and matches the Internal Database URL.
Help me debug my Render build
Seed the database and verify the live app
The build command you configured already includes node seed.js, so the database gets populated with sample data during the first deployment. All you need to do is verify it worked.
- Wait for the deployment status on your service page to show Live.
- Find the URL near the top of the service page. It ends in .onrender.com.
- Click the URL to open your marketplace in a new tab.
You should see the influencer card grid with 12 cards showing names, platform badges, niche tags, follower counts, and star ratings.
- Type Aria in the search bar. The results filter in real time.
- Click the Tech category button. Only Tech influencers appear.
- Click All to reset the filter and see all 12 cards again.
Why does the first load take so long?
Free Render web services spin down after 15 minutes of inactivity. The first request after a spin-down takes about a minute while Render boots the server back up. Subsequent requests are fast until the next idle period.
Not seeing any influencer cards?
- Check the Logs tab on your Render service page. Look for Database seeded successfully! in the build output. If you do not see it, the seed script may have failed.
- Verify the DATABASE_URL environment variable is set correctly. Go to the Environment section in the left sidebar of your service page and confirm the value matches the Internal Database URL from your database.
- If you see a blank white page, the React build may have failed. Check the build logs for errors in the cd ../client && npm install && npm run build part of the build command.
Help me troubleshoot my deployment
Secret mission
Add Sorting and a Detail Modal
Your marketplace lets businesses browse and filter influencers, but real platforms let users sort results and click into detailed profiles. Take your MVP to the next level by adding a sort dropdown and a clickable detail modal.
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses Render's free tier, so the web service costs nothing while running. However, the free PostgreSQL database expires 30 days after creation.
Cost warning
Render's free PostgreSQL database is automatically deleted 30 days after creation. If you want to keep your influencer data permanently, you would need to upgrade to a paid plan (starting at $6/month for the Basic tier). The free web service itself has no cost unless you exceed 750 free instance hours per month.
Resources you used:
- Render Web Service (Free tier): your deployed marketplace app.
- Render PostgreSQL Database (Free tier): your influencer-db database storing influencer records.
- GitHub repository: your project source code.
- Local project folder: the influencer-marketplace directory on your computer.
Keep everything running
No action needed. Choose this if you are still actively building or want to show off your marketplace.
- Your free web service will stay active. It spins down after 15 minutes of inactivity and automatically wakes up on the next request (takes about a minute).
- Your free PostgreSQL database will expire 30 days after creation. Back up your seed script so you can re-seed if needed.
- Your GitHub repository and local project folder remain untouched.
Pause - I'll come back to this later
Render's free tier does not have a manual pause option. Free web services automatically spin down after 15 minutes of inactivity, so they effectively pause themselves.
- Your web service will auto-sleep when idle and wake up when someone visits the URL. No action required.
- Keep in mind that the free PostgreSQL database expires after 30 days. If it expires, you can create a new one and re-run your seed script.
- Your local project folder and GitHub repository will be available whenever you are ready to come back.
Delete - I don't want to use this again
Remove all project resources and start fresh. This permanently deletes your deployed app and database.
- Go to the Render Dashboard.
- Select your web service, scroll to the bottom of the settings page, and click Delete Web Service.
- Select your influencer-db PostgreSQL database, scroll to the bottom, and click Delete Database.
- Optionally delete your GitHub repository from the repository's Settings page under Danger Zone.
- Delete the local influencer-marketplace folder from your computer.
Mission accomplished
That's a Wrap!
Nice work! You just built and deployed a live influencer discovery marketplace. That is a real full-stack application running in the cloud with a React frontend, an Express API, and a PostgreSQL database.
Here's what you learned:
- Built a React + Tailwind CSS frontend with a responsive card grid, real-time search, and category filtering.
- Created a Node.js/Express REST API with parameterized SQL queries against a PostgreSQL database.
- Deployed the full stack to Render as a single web service using the monorepo pattern of serving a React build from an Express server.
- Added sorting and a detail modal to bring the marketplace closer to a production-ready product.
Ready to quiz yourself?