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:
- Defining Resources and Endpoints
- Designing the Database Schema
- Implementing the API Endpoints
- Handling Authentication and Authorization
- Testing the API
- 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 |
- 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 |
| 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 |
- 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:
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:
Repeat similar steps for implementing the routes for Posts, Followers, and Likes.
- Handling Authentication and Authorization
For authentication, we will use JSON Web Tokens (JWT). Install the necessary packages:
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.
- 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.
REST API Course: Principles of Design and Development of RESTful APIs
Module 1: Introduction to RESTful APIs
Module 2: Design of RESTful APIs
- Principles of RESTful API Design
- Resources and URIs
- HTTP Methods
- HTTP Status Codes
- API Versioning
- API Documentation
Module 3: Development of RESTful APIs
- Setting Up the Development Environment
- Creating a Basic Server
- Handling Requests and Responses
- Authentication and Authorization
- Error Handling
- Testing and Validation
Module 4: Best Practices and Security
- Best Practices in API Design
- Security in RESTful APIs
- Rate Limiting and Throttling
- CORS and Security Policies
Module 5: Tools and Frameworks
- Postman for API Testing
- Swagger for Documentation
- Popular Frameworks for RESTful APIs
- Continuous Integration and Deployment
