Project Setup and Foundation
Think of setting up a web application like building a house. We need to start with a strong foundation (our project structure), install the necessary utilities (our dependencies), and create the blueprints (our database schema) before we can start decorating (adding features).
Initial Project Setup
mkdir shopping_app
cd shopping_app
npm init -y
This is like creating the blueprint for our house. The package.json file that's generated is similar to a building permit - it tells others what our project is about and what it needs to function.
Installing Essential Dependencies
npm install express express-session sqlite3 sequelize bcrypt cookie-parser dotenv
npm install --save-dev nodemon
Think of these packages as our construction tools:
- express: The foundation framework (like the concrete foundation of our house)
- sqlite3 & sequelize: Our database tools (like the plumbing system)
- bcrypt: Security system (like installing locks on doors)
- express-session & cookie-parser: User session management (like a guest registry system)
- dotenv: Environment configuration (like the building's control panel)
Project Structure
Let's organize our project like rooms in a house - each with its specific purpose:
shopping_app/
├── config/
│ └── database.js
├── models/
│ ├── index.js
│ └── user.js
├── routes/
│ ├── index.js
│ └── auth.js
├── middleware/
│ └── auth.js
├── .env
└── app.js
Setting Up the Database Configuration
First, create a .env file in your root directory:
# .env
DB_NAME=shopping_db
SESSION_SECRET=your_secret_key_here
Now, let's create our database configuration (config/database.js):
// config/database.js
const { Sequelize } = require('sequelize');
require('dotenv').config();
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './database.sqlite',
logging: false
});
module.exports = sequelize;
Creating the User Model
Think of a model like a blueprint for a type of object in our system. The User model is like a form that specifies what information we need about each user:
// models/user.js
const { Model, DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const bcrypt = require('bcrypt');
class User extends Model {
async validatePassword(password) {
return bcrypt.compare(password, this.password);
}
}
User.init({
email: {
type: DataTypes.STRING,
unique: true,
allowNull: false,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
}
}, {
sequelize,
modelName: 'User',
hooks: {
beforeCreate: async (user) => {
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(user.password, salt);
}
}
});
module.exports = User;
Setting Up Authentication Routes
Routes are like the hallways in our house - they direct traffic to the right rooms:
// routes/auth.js
const router = require('express').Router();
const User = require('../models/user');
router.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
const user = await User.create({ email, password, name });
req.session.userId = user.id;
res.json({ message: 'Registration successful' });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const validPassword = await user.validatePassword(password);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
req.session.userId = user.id;
res.json({ message: 'Login successful' });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
router.post('/logout', (req, res) => {
req.session.destroy();
res.json({ message: 'Logged out successfully' });
});
module.exports = router;
Creating the Main Application File
The app.js file is like the central control system of our house, connecting all the pieces together:
// app.js
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
require('dotenv').config();
const sequelize = require('./config/database');
const authRoutes = require('./routes/auth');
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: false } // Set to true in production with HTTPS
}));
app.use('/auth', authRoutes);
// Test route
app.get('/', (req, res) => {
res.json({ message: 'Shopping app API is running' });
});
const PORT = process.env.PORT || 3000;
async function startServer() {
try {
await sequelize.sync();
console.log('Database synchronized successfully');
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
} catch (error) {
console.error('Unable to start server:', error);
}
}
startServer();
Testing the Application
Add this script to your package.json:
{
"scripts": {
"dev": "nodemon app.js"
}
}
Start the application:
npm run dev
Test the endpoints using curl or Postman:
# Register a new user
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123","name":"Test User"}'
# Login
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123"}'
Next Steps and Security Considerations
Now that we have our basic authentication system in place, consider implementing:
- Password reset functionality
- Email verification
- Rate limiting for login attempts
- CSRF protection
- Input validation middleware
- Error handling middleware
Security Best Practices
Remember to:
- Use HTTPS in production
- Set secure cookie options in production
- Implement proper logging
- Regular security audits
- Keep dependencies updated