Introduction
In this tutorial, we'll explore how to build a scalable marketplace API using Node.js and Express that can handle the kind of growth that companies like Vinted are experiencing. We'll focus on creating a robust backend system that can support product listings, user management, and transaction processing - core components of any secondhand marketplace platform. This tutorial will teach you how to structure a modern API, implement database relationships, and handle common marketplace operations.
Prerequisites
- Basic understanding of JavaScript and Node.js
- Node.js installed (version 14 or higher)
- npm or yarn package manager
- Basic knowledge of RESTful API design
- SQLite or PostgreSQL database installed
- Postman or similar API testing tool
Step 1: Initialize Project and Install Dependencies
Why this step matters
We start by creating a clean project structure and installing all necessary packages. This sets up our foundation for building a scalable marketplace API.
mkdir vinted-api
cd vinted-api
npm init -y
npm install express cors helmet morgan dotenv
npm install -D nodemon
Step 2: Create Basic Server Structure
Why this step matters
Setting up the basic server configuration with middleware ensures our API is secure, logs requests properly, and handles cross-origin requests.
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(helmet());
app.use(cors());
app.use(morgan('combined'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes will go here
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 3: Set Up Database Models
Why this step matters
Marketplace platforms require robust data models for users, products, and transactions. We'll create a basic database structure that supports these core entities.
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database(':memory:');
// Create users table
const createUserTable = `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`;
// Create products table
const createProductTable = `CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
price REAL NOT NULL,
user_id INTEGER,
category TEXT,
condition TEXT,
status TEXT DEFAULT 'available',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)`;
// Execute table creation
db.serialize(() => {
db.run(createUserTable);
db.run(createProductTable);
});
Step 4: Implement User Authentication Routes
Why this step matters
Secure user authentication is crucial for any marketplace platform. We'll implement basic login and registration endpoints with proper error handling.
const bcrypt = require('bcrypt');
// User registration route
app.post('/api/register', async (req, res) => {
try {
const { username, email, password } = req.body;
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Insert user into database
const stmt = db.prepare("INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)");
stmt.run(username, email, hashedPassword);
stmt.finalize();
res.status(201).json({ message: 'User created successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// User login route
app.post('/api/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user
db.get("SELECT * FROM users WHERE email = ?", [email], async (err, user) => {
if (err) return res.status(500).json({ error: err.message });
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
// Verify password
const isValid = await bcrypt.compare(password, user.password_hash);
if (!isValid) return res.status(401).json({ error: 'Invalid credentials' });
// In a real app, we would generate a JWT token here
res.json({ message: 'Login successful', user: { id: user.id, username: user.username } });
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Step 5: Create Product Management Endpoints
Why this step matters
Product listings are the core of any marketplace. We'll implement CRUD operations for products with proper validation and user association.
// Create product
app.post('/api/products', (req, res) => {
try {
const { title, description, price, category, condition, user_id } = req.body;
const stmt = db.prepare("INSERT INTO products (title, description, price, category, condition, user_id) VALUES (?, ?, ?, ?, ?, ?)");
stmt.run(title, description, price, category, condition, user_id);
stmt.finalize();
res.status(201).json({ message: 'Product created successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get all products
app.get('/api/products', (req, res) => {
try {
const { page = 1, limit = 10, category, status } = req.query;
let query = 'SELECT * FROM products WHERE status = ?';
let params = ['available'];
if (category) {
query += ' AND category = ?';
params.push(category);
}
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
params.push(limit, (page - 1) * limit);
db.all(query, params, (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Step 6: Add Transaction Handling
Why this step matters
Marketplace platforms need to handle transactions securely. We'll implement basic transaction logic with proper status tracking.
// Create transaction
app.post('/api/transactions', (req, res) => {
try {
const { product_id, buyer_id, seller_id, amount } = req.body;
// Create transaction record
const stmt = db.prepare("INSERT INTO transactions (product_id, buyer_id, seller_id, amount, status) VALUES (?, ?, ?, ?, ?)");
stmt.run(product_id, buyer_id, seller_id, amount, 'pending');
stmt.finalize();
// Update product status
db.run("UPDATE products SET status = 'sold' WHERE id = ?", [product_id]);
res.status(201).json({ message: 'Transaction created successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get user transactions
app.get('/api/transactions/user/:user_id', (req, res) => {
try {
const { user_id } = req.params;
db.all("SELECT * FROM transactions WHERE buyer_id = ? OR seller_id = ? ORDER BY created_at DESC", [user_id, user_id], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Step 7: Test Your API
Why this step matters
Testing ensures our API works correctly and handles various scenarios. We'll use Postman to test our endpoints.
- Start your server:
npm run dev - Use Postman to test the following endpoints:
- POST /api/register - Test user registration
- POST /api/login - Test user authentication
- POST /api/products - Test product creation
- GET /api/products - Test product listing
- POST /api/transactions - Test transaction creation
Summary
In this tutorial, we've built a foundational marketplace API with core features like user management, product listings, and transaction handling. While this is a simplified version, it demonstrates the key architectural patterns used in platforms like Vinted. The code structure we've implemented can be extended with features like JWT authentication, payment processing, image uploads, and real-time notifications to support the scale and complexity of a growing marketplace platform.
Remember that real-world applications would require additional security measures, database optimization, caching, and more sophisticated error handling. This tutorial provides the essential building blocks for understanding how marketplace platforms scale and handle their core operations.



