In this case study, we will design and develop a RESTful API for a social network. This API will allow users to perform various actions such as creating profiles, posting updates, following other users, and liking posts. We will cover the following key aspects:

  1. Defining Resources and Endpoints
  2. Designing the Database Schema
  3. Implementing the API Endpoints
  4. Handling Authentication and Authorization
  5. Testing the API

  1. Defining Resources and Endpoints

Resources

The main resources for our social network API are:

  • Users: Represents the users of the social network.
  • Posts: Represents the posts created by users.
  • Followers: Represents the relationship between users (who follows whom).
  • Likes: Represents the likes on posts.

Endpoints

Here are the endpoints we will implement:

Resource Endpoint HTTP Method Description
Users /users GET Retrieve a list of users
Users /users POST Create a new user
Users /users/{id} GET Retrieve a specific user
Users /users/{id} PUT Update a specific user
Users /users/{id} DELETE Delete a specific user
Posts /posts GET Retrieve a list of posts
Posts /posts POST Create a new post
Posts /posts/{id} GET Retrieve a specific post
Posts /posts/{id} PUT Update a specific post
Posts /posts/{id} DELETE Delete a specific post
Followers /users/{id}/followers GET Retrieve a list of followers for a user
Followers /users/{id}/follow POST Follow a user
Followers /users/{id}/unfollow POST Unfollow a user
Likes /posts/{id}/likes GET Retrieve a list of likes for a post
Likes /posts/{id}/like POST Like a post
Likes /posts/{id}/unlike POST Unlike a post

  1. Designing the Database Schema

We will use a relational database for this case study. Below is the schema for our database:

Users Table

Column Type Description
id INT Primary key
username VARCHAR Unique username
email VARCHAR Unique email address
password VARCHAR Hashed password
created_at DATETIME Timestamp of creation

Posts Table

Column Type Description
id INT Primary key
user_id INT Foreign key to Users
content TEXT Content of the post
created_at DATETIME Timestamp of creation

Followers Table

Column Type Description
id INT Primary key
user_id INT Foreign key to Users
follower_id INT Foreign key to Users

Likes Table

Column Type Description
id INT Primary key
post_id INT Foreign key to Posts
user_id INT Foreign key to Users

  1. Implementing the API Endpoints

Setting Up the Development Environment

We will use Node.js and Express for our API. First, set up a new Node.js project and install the necessary dependencies:

mkdir social-network-api
cd social-network-api
npm init -y
npm install express body-parser mongoose

Creating a Basic Server

Create a file named server.js and set up a basic Express server:

const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');

const app = express();
app.use(bodyParser.json());

mongoose.connect('mongodb://localhost:27017/socialnetwork', { useNewUrlParser: true, useUnifiedTopology: true });

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

Defining Models

Create a directory named models and define the models for Users, Posts, Followers, and Likes.

User Model (models/User.js)

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
    username: { type: String, required: true, unique: true },
    email: { type: String, required: true, unique: true },
    password: { type: String, required: true },
    created_at: { type: Date, default: Date.now }
});

module.exports = mongoose.model('User', userSchema);

Post Model (models/Post.js)

const mongoose = require('mongoose');

const postSchema = new mongoose.Schema({
    user_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
    content: { type: String, required: true },
    created_at: { type: Date, default: Date.now }
});

module.exports = mongoose.model('Post', postSchema);

Follower Model (models/Follower.js)

const mongoose = require('mongoose');

const followerSchema = new mongoose.Schema({
    user_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
    follower_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }
});

module.exports = mongoose.model('Follower', followerSchema);

Like Model (models/Like.js)

const mongoose = require('mongoose');

const likeSchema = new mongoose.Schema({
    post_id: { type: mongoose.Schema.Types.ObjectId, ref: 'Post', required: true },
    user_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }
});

module.exports = mongoose.model('Like', likeSchema);

Implementing Endpoints

User Endpoints

Create a directory named routes and define the routes for users in routes/users.js:

const express = require('express');
const router = express.Router();
const User = require('../models/User');

// Get all users
router.get('/', async (req, res) => {
    try {
        const users = await User.find();
        res.json(users);
    } catch (err) {
        res.status(500).json({ message: err.message });
    }
});

// Create a new user
router.post('/', async (req, res) => {
    const user = new User({
        username: req.body.username,
        email: req.body.email,
        password: req.body.password
    });

    try {
        const newUser = await user.save();
        res.status(201).json(newUser);
    } catch (err) {
        res.status(400).json({ message: err.message });
    }
});

// Get a specific user
router.get('/:id', getUser, (req, res) => {
    res.json(res.user);
});

// Update a specific user
router.put('/:id', getUser, async (req, res) => {
    if (req.body.username != null) {
        res.user.username = req.body.username;
    }
    if (req.body.email != null) {
        res.user.email = req.body.email;
    }
    if (req.body.password != null) {
        res.user.password = req.body.password;
    }

    try {
        const updatedUser = await res.user.save();
        res.json(updatedUser);
    } catch (err) {
        res.status(400).json({ message: err.message });
    }
});

// Delete a specific user
router.delete('/:id', getUser, async (req, res) => {
    try {
        await res.user.remove();
        res.json({ message: 'Deleted User' });
    } catch (err) {
        res.status(500).json({ message: err.message });
    }
});

// Middleware to get user by ID
async function getUser(req, res, next) {
    let user;
    try {
        user = await User.findById(req.params.id);
        if (user == null) {
            return res.status(404).json({ message: 'Cannot find user' });
        }
    } catch (err) {
        return res.status(500).json({ message: err.message });
    }

    res.user = user;
    next();
}

module.exports = router;

Integrating Routes

In server.js, integrate the user routes:

const userRouter = require('./routes/users');
app.use('/users', userRouter);

Repeat similar steps for implementing the routes for Posts, Followers, and Likes.

  1. Handling Authentication and Authorization

For authentication, we will use JSON Web Tokens (JWT). Install the necessary packages:

npm install jsonwebtoken bcryptjs

User Registration and Login

Update the user routes to include registration and login:

const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

// Register a new user
router.post('/register', async (req, res) => {
    const hashedPassword = await bcrypt.hash(req.body.password, 10);
    const user = new User({
        username: req.body.username,
        email: req.body.email,
        password: hashedPassword
    });

    try {
        const newUser = await user.save();
        res.status(201).json(newUser);
    } catch (err) {
        res.status(400).json({ message: err.message });
    }
});

// Login a user
router.post('/login', async (req, res) => {
    const user = await User.findOne({ email: req.body.email });
    if (user == null) {
        return res.status(400).json({ message: 'Cannot find user' });
    }

    try {
        if (await bcrypt.compare(req.body.password, user.password)) {
            const token = jwt.sign({ id: user._id }, 'SECRET_KEY');
            res.json({ token });
        } else {
            res.status(400).json({ message: 'Invalid credentials' });
        }
    } catch (err) {
        res.status(500).json({ message: err.message });
    }
});

Protecting Routes

Create a middleware to protect routes:

function authenticateToken(req, res, next) {
    const token = req.header('Authorization');
    if (!token) return res.status(401).json({ message: 'Access Denied' });

    try {
        const verified = jwt.verify(token, 'SECRET_KEY');
        req.user = verified;
        next();
    } catch (err) {
        res.status(400).json({ message: 'Invalid Token' });
    }
}

Use this middleware to protect routes that require authentication.

  1. Testing the API

Use Postman to test the API endpoints. Create a collection and add requests for each endpoint. Ensure that you test both successful and error scenarios.

Conclusion

In this case study, we designed and developed a RESTful API for a social network. We covered defining resources and endpoints, designing the database schema, implementing the API endpoints, handling authentication and authorization, and testing the API. This comprehensive example provides a solid foundation for building more complex APIs in the future.

© Copyright 2024. All rights reserved