Express.js REST API
Build CRUD APIs, test with Postman, and follow REST best practices
CRUD
HTTP Methods
Postman
Status Codes
1. Understanding REST API Basics
1.1 What is an API?
An API (Application Programming Interface) is a set of rules that allows different software applications to communicate with each other. Think of it as a waiter in a restaurant:
- You (Client) → Place an order (Request)
- Waiter (API) → Takes order to kitchen
- Kitchen (Server) → Prepares food
- Waiter (API) → Brings food back (Response)
In web development, APIs allow frontend applications (mobile apps, websites) to interact with backend servers without knowing how they're built internally.
1.2 What is REST?
REST (Representational State Transfer) is an architectural style for designing networked applications. It was introduced by Roy Fielding in 2000. REST APIs (also called RESTful APIs) follow specific principles that make them scalable, simple, and reliable.
Analogy: Think of REST as a library system. You can:
GET /books → See all books
GET /books/123 → See a specific book
POST /books → Add a new book
PUT /books/123 → Replace a book entirely
PATCH /books/123 → Update part of a book
DELETE /books/123 → Remove a book
1.3 REST Architectural Constraints
For an API to be truly RESTful, it must follow these 6 constraints:
| Constraint | Description | Example |
| Client-Server | Separation of concerns | Mobile app (client) communicates with server |
| Stateless | Each request contains all necessary info | Server doesn't remember previous requests |
| Cacheable | Responses must define cacheability | Static data can be cached for 1 hour |
| Layered System | Client can't tell if it's talking to the end server | Load balancers, proxies in between |
| Uniform Interface | Standard methods and conventions | Using standard HTTP methods, status codes |
| Code-on-Demand (optional) | Server can send executable code | JavaScript, Java applets |
1.4 HTTP Methods and Their Meanings
HTTP methods (verbs) indicate the desired action to perform on a resource:
| Method | CRUD Action | Description | Example | Success Code |
| GET | Read | Retrieve data (should never modify) | GET /users/123 | 200 OK |
| POST | Create | Create a new resource | POST /users | 201 Created |
| PUT | Update/Replace | Replace an entire resource | PUT /users/123 | 200 OK |
| PATCH | Update/Modify | Partially update a resource | PATCH /users/123 | 200 OK |
| DELETE | Delete | Remove a resource | DELETE /users/123 | 204 No Content |
| HEAD | — | Same as GET but only headers | HEAD /users | 200 OK |
| OPTIONS | — | Describe communication options | OPTIONS /users | 200 OK |
PUT vs PATCH:
- PUT replaces the entire resource. Send all fields.
- PATCH updates only specified fields. Send only what changed.
JavaScript// PUT - Replace entire user
// Sending: { "name": "John", "email": "john@example.com", "age": 30 }
// Result: Previous user data is completely replaced
// PATCH - Update only age
// Sending: { "age": 31 }
// Result: Only age changes, name and email remain same
1.5 HTTP Status Codes (The Essentials)
Status codes tell the client what happened with their request. They're grouped into five classes:
2xx — Success (Everything worked)
| Code | Meaning | When to Use |
| 200 | OK | Standard success response for GET, PUT, PATCH |
| 201 | Created | After successful POST (new resource created) |
| 204 | No Content | After successful DELETE (no response body needed) |
3xx — Redirection (Further action needed)
| Code | Meaning | When to Use |
| 301 | Moved Permanently | Resource URL has changed permanently |
| 304 | Not Modified | Use cached version (caching headers) |
4xx — Client Error (Request problem)
| Code | Meaning | When to Use |
| 400 | Bad Request | Invalid data format, missing required fields |
| 401 | Unauthorized | Authentication required or failed |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Resource already exists (e.g., duplicate email) |
| 422 | Unprocessable Entity | Valid format but semantic errors |
5xx — Server Error (Server problem)
| Code | Meaning | When to Use |
| 500 | Internal Server Error | Unexpected server error (catch-all) |
| 501 | Not Implemented | Feature not implemented |
| 503 | Service Unavailable | Server overloaded or maintenance |
1.6 REST API Endpoint Naming Conventions
Good endpoint naming makes APIs intuitive and self-documenting:
✅ DO: Use nouns (not verbs)
ExamplesGET /users ✓ Good
GET /getUsers ✗ Bad (verb in URL)
POST /users ✓ Good
POST /createUser ✗ Bad
✅ DO: Use plural nouns for collections
ExamplesGET /users/123 ✓ Good
GET /user/123 ✗ Bad (singular)
✅ DO: Use nested resources for relationships
ExamplesGET /users/123/posts Get posts by user 123
POST /users/123/posts Create post for user 123
GET /posts/456/comments Get comments for post 456
✅ DO: Use query parameters for filtering
ExamplesGET /users?status=active Filter active users
GET /users?page=2&limit=10 Pagination
GET /users?sort=name&order=asc Sorting
❌ DON'T: Use file extensions
ExamplesGET /users.json ✗ Bad
GET /users ✓ Good (use Accept header)
1.7 Request and Response Structure
Typical REST API Request:
HTTP RequestGET /api/users/123 HTTP/1.1
Host: example.com
Authorization: Bearer token123
Accept: application/json
Content-Type: application/json
{
"name": "John Doe"
}
Typical REST API Response:
HTTP ResponseHTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=3600
{
"status": "success",
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com"
},
"message": "User retrieved successfully"
}
Response Body Format Best Practice:
JavaScript// Success Response Format
{
"status": "success",
"data": { ... }, // Single object or array
"message": "Optional message",
"meta": { // Optional metadata for collections
"total": 100,
"page": 1,
"pages": 10
}
}
// Error Response Format
{
"status": "error",
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "User with id 123 not found",
"details": { ... } // Optional validation details
}
}
2. Building a CRUD API with Express
2.1 Project Setup and Dependencies
Bashmkdir crud-api-demo
cd crud-api-demo
npm init -y
npm install express
npm install -D nodemon # For auto-restart during development
package.json scripts:
JSON{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
}
Basic server setup (server.js):
JavaScriptconst express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use((req, res, next) => {
console.log(`${req.method} ${req.path} - ${new Date().toISOString()}`);
next();
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
2.2 Understanding CRUD Operations
| Operation | HTTP Method | SQL Equivalent | Mongoose (MongoDB) |
| Create | POST | INSERT | .save() or .create() |
| Read | GET | SELECT | .find() or .findById() |
| Update | PUT/PATCH | UPDATE | .findByIdAndUpdate() |
| Delete | DELETE | DELETE | .findByIdAndDelete() |
2.3 CREATE (POST) – Adding New Resources
JavaScriptlet books = [
{ id: 1, title: '1984', author: 'George Orwell', year: 1949 },
{ id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 }
];
let nextId = 3;
app.post('/api/books', (req, res) => {
const { title, author, year } = req.body;
if (!title || !author) {
return res.status(400).json({
status: 'error',
message: 'Title and author are required fields'
});
}
const newBook = { id: nextId++, title, author, year: year || null };
books.push(newBook);
res.status(201).json({
status: 'success',
data: newBook,
message: 'Book created successfully'
});
});
2.4 READ (GET) – Retrieving Resources
JavaScriptapp.get('/api/books', (req, res) => {
res.json({ status: 'success', data: books, count: books.length });
});
app.get('/api/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const book = books.find(b => b.id === id);
if (!book) {
return res.status(404).json({
status: 'error',
message: `Book with id ${id} not found`
});
}
res.json({ status: 'success', data: book });
});
app.get('/api/books/filter/by-author', (req, res) => {
const { author } = req.query;
if (!author) {
return res.status(400).json({
status: 'error',
message: 'Author query parameter is required'
});
}
const filteredBooks = books.filter(book =>
book.author.toLowerCase().includes(author.toLowerCase())
);
res.json({
status: 'success',
data: filteredBooks,
count: filteredBooks.length
});
});
2.5 UPDATE (PUT/PATCH) – Modifying Resources
JavaScript — PUTapp.put('/api/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const { title, author, year } = req.body;
const bookIndex = books.findIndex(b => b.id === id);
if (bookIndex === -1) {
return res.status(404).json({
status: 'error',
message: `Book with id ${id} not found`
});
}
if (!title || !author) {
return res.status(400).json({
status: 'error',
message: 'PUT requires title and author fields'
});
}
books[bookIndex] = { id, title, author, year: year || null };
res.json({
status: 'success',
data: books[bookIndex],
message: 'Book replaced successfully'
});
});
JavaScript — PATCHapp.patch('/api/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const updates = req.body;
const book = books.find(b => b.id === id);
if (!book) {
return res.status(404).json({
status: 'error',
message: `Book with id ${id} not found`
});
}
if (updates.title) book.title = updates.title;
if (updates.author) book.author = updates.author;
if (updates.year !== undefined) book.year = updates.year;
res.json({
status: 'success',
data: book,
message: 'Book updated successfully'
});
});
2.6 DELETE (DELETE) – Removing Resources
JavaScriptapp.delete('/api/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const bookIndex = books.findIndex(b => b.id === id);
if (bookIndex === -1) {
return res.status(404).json({
status: 'error',
message: `Book with id ${id} not found`
});
}
books.splice(bookIndex, 1);
res.status(204).send();
});
// Soft delete
let deletedBooks = [];
app.delete('/api/books/:id/soft', (req, res) => {
const id = parseInt(req.params.id);
const bookIndex = books.findIndex(b => b.id === id);
if (bookIndex === -1) {
return res.status(404).json({
status: 'error',
message: `Book with id ${id} not found`
});
}
const deletedBook = books.splice(bookIndex, 1)[0];
deletedBooks.push({ ...deletedBook, deletedAt: new Date().toISOString() });
res.json({
status: 'success',
message: 'Book soft deleted successfully',
data: deletedBook
});
});
2.7 Complete CRUD API Example (Books API)
Here's a complete, production-ready CRUD API with all endpoints:
JavaScript — Complete Books APIconst express = require('express');
const app = express();
app.use(express.json());
let books = [
{ id: 1, title: '1984', author: 'George Orwell', year: 1949, isbn: '978-0451524935' },
{ id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960, isbn: '978-0061120084' }
];
let nextId = 3;
const findBookIndex = (id) => books.findIndex(book => book.id === id);
app.post('/api/books', (req, res) => {
const { title, author, year, isbn } = req.body;
const errors = [];
if (!title) errors.push('Title is required');
if (!author) errors.push('Author is required');
if (year && (typeof year !== 'number' || year < 0 || year > new Date().getFullYear())) {
errors.push('Year must be a valid year');
}
if (errors.length > 0) {
return res.status(400).json({ status: 'error', errors });
}
const newBook = {
id: nextId++, title, author,
year: year || null, isbn: isbn || null,
createdAt: new Date().toISOString()
};
books.push(newBook);
res.status(201).json({ status: 'success', data: newBook });
});
app.get('/api/books', (req, res) => {
let result = [...books];
if (req.query.author) {
result = result.filter(book =>
book.author.toLowerCase().includes(req.query.author.toLowerCase())
);
}
if (req.query.minYear) {
result = result.filter(book => book.year >= parseInt(req.query.minYear));
}
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const startIndex = (page - 1) * limit;
const paginatedResult = result.slice(startIndex, page * limit);
res.json({
status: 'success',
data: paginatedResult,
pagination: {
total: result.length, page, limit,
pages: Math.ceil(result.length / limit)
}
});
});
app.get('/api/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const book = books.find(b => b.id === id);
if (!book) {
return res.status(404).json({ status: 'error', message: `Book with ID ${id} not found` });
}
res.json({ status: 'success', data: book });
});
app.put('/api/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const { title, author, year, isbn } = req.body;
const index = findBookIndex(id);
if (index === -1) {
return res.status(404).json({ status: 'error', message: `Book with ID ${id} not found` });
}
if (!title || !author) {
return res.status(400).json({ status: 'error', message: 'Title and author are required' });
}
books[index] = {
id, title, author,
year: year || books[index].year,
isbn: isbn || books[index].isbn,
updatedAt: new Date().toISOString()
};
res.json({ status: 'success', data: books[index] });
});
app.patch('/api/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const updates = req.body;
const book = books.find(b => b.id === id);
if (!book) {
return res.status(404).json({ status: 'error', message: `Book with ID ${id} not found` });
}
const allowedUpdates = ['title', 'author', 'year', 'isbn'];
const requestedUpdates = Object.keys(updates);
const isValidOperation = requestedUpdates.every(u => allowedUpdates.includes(u));
if (!isValidOperation) {
return res.status(400).json({ status: 'error', message: 'Invalid update fields' });
}
requestedUpdates.forEach(update => {
if (updates[update] !== undefined) book[update] = updates[update];
});
book.updatedAt = new Date().toISOString();
res.json({ status: 'success', data: book });
});
app.delete('/api/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = findBookIndex(id);
if (index === -1) {
return res.status(404).json({ status: 'error', message: `Book with ID ${id} not found` });
}
books.splice(index, 1);
res.status(204).send();
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ status: 'error', message: 'Something went wrong on the server' });
});
app.use((req, res) => {
res.status(404).json({ status: 'error', message: `Cannot ${req.method} ${req.url}` });
});
app.listen(3000, () => console.log('Books API running on http://localhost:3000'));
3. Testing APIs with Postman
3.1 What is Postman?
Postman is a popular API development and testing tool that allows you to:
- Send HTTP requests (GET, POST, PUT, DELETE, etc.)
- View responses with formatting
- Organize requests into collections
- Automate testing with scripts
- Share APIs with team members
- Generate documentation
3.2 Installation and Setup
Installation:
- Download from postman.com/downloads
- Install for your operating system
- Create a free account (optional, but recommended for cloud sync)
Postman Interface Overview:
- Sidebar: Collections, History, APIs
- Request Builder: URL bar, method selector, tabs (Params, Authorization, Headers, Body)
- Response Area: Status code, time, size, body preview
- Console: View logs and debug information
3.3 Making GET Requests
- Select GET method from dropdown
- Enter URL:
http://localhost:3000/api/books
- Click Send
Expected Response[GET] ▼ http://localhost:3000/api/books [Send]
Status: 200 OK
Body:
{
"status": "success",
"data": [
{ "id": 1, "title": "1984", "author": "George Orwell", "year": 1949 },
{ "id": 2, "title": "To Kill a Mockingbird", "author": "Harper Lee", "year": 1960 }
],
"count": 2
}
GET with query parameters: URL: http://localhost:3000/api/books?author=Orwell — click Params tab and add Key: author, Value: Orwell.
3.4 Making POST Requests (Sending Data)
- Select POST method
- URL:
http://localhost:3000/api/books
- Select Body tab → choose raw and JSON
- Enter JSON data and click Send
JSON Request Body{
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"year": 1925,
"isbn": "978-0743273565"
}
Expected Response (201 Created){
"status": "success",
"data": {
"id": 3,
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"year": 1925,
"isbn": "978-0743273565",
"createdAt": "2024-01-15T10:30:00.000Z"
}
}
Testing validation errors:
JSON — Missing Fields (400 Bad Request)// Request: { "title": "Incomplete Book" }
// Response:
{
"status": "error",
"errors": ["Author is required"]
}
3.5 Making PUT/PATCH Requests
PUT Request (Full Update): Method PUT, URL http://localhost:3000/api/books/3
JSON{
"title": "The Great Gatsby - Updated",
"author": "F. Scott Fitzgerald",
"year": 1925,
"isbn": "978-0743273565"
}
PATCH Request (Partial Update): Method PATCH, URL http://localhost:3000/api/books/3
JSON{
"title": "The Great Gatsby (Revised Edition)"
}
3.6 Making DELETE Requests
- Method: DELETE
- URL:
http://localhost:3000/api/books/3
- Expected: Status 204 No Content, empty body
3.7 Working with Environments and Variables
Instead of typing http://localhost:3000 every time, use {{baseUrl}}.
- Click Environments → create Development
- Add variable:
baseUrl = http://localhost:3000
- Use in URL:
{{baseUrl}}/api/books
Postman Tests Tab// Save the ID of newly created resource
const response = pm.response.json();
pm.environment.set("bookId", response.data.id);
// Then use: {{baseUrl}}/api/books/{{bookId}}
3.8 Creating Collections for Organized Testing
Collection Structure📁 Books API Tests
├── 📁 Books
│ ├── 📄 GET All Books
│ ├── 📄 GET Single Book
│ ├── 📄 POST Create Book
│ ├── 📄 PUT Update Book
│ ├── 📄 PATCH Partial Update
│ └── 📄 DELETE Remove Book
└── 📁 Validation Tests
├── 📄 POST Missing Fields
└── 📄 GET Non-existent Book
3.9 Writing Basic Tests in Postman
JavaScript — Test Script Structurepm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Response has correct structure", function () {
const jsonData = pm.response.json();
pm.expect(jsonData).to.have.property("status");
pm.expect(jsonData.status).to.equal("success");
});
GET All Books Testspm.test("Status code is 200", () => pm.response.to.have.status(200));
pm.test("Response is JSON", () => pm.response.to.be.json);
pm.test("Data is an array", () => {
const response = pm.response.json();
pm.expect(response.data).to.be.an("array");
});
pm.test("Book '1984' exists", () => {
const books = pm.response.json().data;
const book1984 = books.find(b => b.title === "1984");
pm.expect(book1984).to.exist;
pm.expect(book1984.author).to.equal("George Orwell");
});
POST Create Book Testspm.test("Status code is 201 Created", () => pm.response.to.have.status(201));
pm.test("Save book ID to environment", () => {
const response = pm.response.json();
pm.environment.set("lastCreatedId", response.data.id);
});
DELETE Book Testspm.test("Status code is 204 No Content", () => {
pm.response.to.have.status(204);
});
4. Advanced REST API Concepts
4.1 Request Query Parameters (Filtering, Sorting, Pagination)
JavaScriptapp.get('/api/books', (req, res) => {
let query = [...books];
if (req.query.author) {
query = query.filter(book => book.author === req.query.author);
}
if (req.query.search) {
const searchTerm = req.query.search.toLowerCase();
query = query.filter(book =>
book.title.toLowerCase().includes(searchTerm) ||
book.author.toLowerCase().includes(searchTerm)
);
}
if (req.query.minYear) {
query = query.filter(book => book.year >= parseInt(req.query.minYear));
}
if (req.query.maxYear) {
query = query.filter(book => book.year <= parseInt(req.query.maxYear));
}
if (req.query.sort) {
const sortField = req.query.sort;
const sortOrder = req.query.order === 'desc' ? -1 : 1;
query.sort((a, b) => {
if (a[sortField] < b[sortField]) return -1 * sortOrder;
if (a[sortField] > b[sortField]) return 1 * sortOrder;
return 0;
});
}
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const startIndex = (page - 1) * limit;
const endIndex = page * limit;
const total = query.length;
const paginatedQuery = query.slice(startIndex, endIndex);
res.json({
status: 'success',
count: paginatedQuery.length,
data: paginatedQuery,
pagination: {
total, page, limit,
pages: Math.ceil(total / limit),
next: endIndex < total ? page + 1 : null,
prev: startIndex > 0 ? page - 1 : null
}
});
});
// GET /api/books?author=Orwell&minYear=1940&maxYear=1960&sort=year&order=asc&page=1&limit=5
JavaScriptconst authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
status: 'error',
message: 'No token provided or invalid format'
});
}
const token = authHeader.split(' ')[1];
if (token !== 'secret-token-123') {
return res.status(401).json({ status: 'error', message: 'Invalid authentication token' });
}
req.user = { id: 1, name: 'Admin User', role: 'admin' };
next();
};
app.post('/api/admin/books', authenticate, (req, res) => {
res.json({
status: 'success',
message: `Book created by ${req.user.name}`,
user: req.user
});
});
app.get('/api/secure-data', (req, res) => {
res.set({
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Cache-Control': 'no-store, no-cache, must-revalidate, private'
});
res.json({ secret: 'This is protected data' });
});
4.3 Error Handling in REST APIs
JavaScriptclass AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message, details) {
super(message, 400);
this.details = details;
this.name = 'ValidationError';
}
}
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} with id ${id} not found`, 404);
}
}
const catchAsync = (fn) => (req, res, next) => fn(req, res, next).catch(next);
app.get('/api/books/:id', catchAsync(async (req, res) => {
const id = parseInt(req.params.id);
const book = books.find(b => b.id === id);
if (!book) throw new NotFoundError('Book', id);
res.json({ status: 'success', data: book });
}));
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
console.error('Error:', { message: err.message, url: req.url, method: req.method });
if (err.isOperational) {
res.status(err.statusCode).json({
status: err.status,
message: err.message,
...(err.details && { details: err.details })
});
} else {
res.status(500).json({ status: 'error', message: 'Something went wrong!' });
}
});
4.4 API Versioning Strategies
Strategy 1: URL Path Versioning (Most Common)
JavaScriptapp.get('/api/v1/books', (req, res) => {
res.json(books.map(b => ({ id: b.id, title: b.title })));
});
app.get('/api/v2/books', (req, res) => {
res.json(books);
});
app.get('/api/v3/books', (req, res) => {
res.json(books.map(b => ({
...b,
_links: {
self: `/api/v3/books/${b.id}`,
author: `/api/v3/authors/${b.authorId}`
}
})));
});
Strategy 2: Custom Header Versioning
JavaScriptapp.get('/api/books', (req, res) => {
const version = req.headers['api-version'] || '1.0';
switch(version) {
case '1.0':
return res.json(books.map(b => ({ id: b.id, title: b.title })));
case '2.0':
return res.json(books);
default:
return res.status(400).json({ status: 'error', message: 'Unsupported API version' });
}
});
// Client: GET /api/books Headers: { "api-version": "2.0" }
Strategy 3: Accept Header Versioning
JavaScriptapp.get('/api/books', (req, res) => {
const acceptHeader = req.headers.accept || '';
if (acceptHeader.includes('application/vnd.myapi.v2+json')) {
return res.json(books);
} else if (acceptHeader.includes('application/vnd.myapi.v1+json')) {
return res.json(books.map(b => ({ id: b.id, title: b.title })));
}
return res.json(books);
});
// Client: Headers: { "Accept": "application/vnd.myapi.v2+json" }
5. Complete Project: Library Management API
The Books API in section 2.7 serves as a complete Library Management API. It implements all CRUD endpoints with validation, pagination, filtering, and error handling.
API Endpoints:
| Method | Endpoint | Description |
| GET | /api/books | List all books (with filtering & pagination) |
| GET | /api/books/:id | Get a single book by ID |
| POST | /api/books | Add a new book to the library |
| PUT | /api/books/:id | Replace a book entirely |
| PATCH | /api/books/:id | Partially update a book |
| DELETE | /api/books/:id | Remove a book from the library |
Test the full workflow in Postman using the collection structure from section 3.8. Start the server with npm run dev, then run through create → read → update → delete operations.
6. Best Practices for REST API Development
6.1 Naming and Structure
| Practice | Bad Example | Good Example |
| Use nouns, not verbs | /getBooks | /books |
| Use plural for collections | /book | /books |
| Use nested for relationships | /getBookComments?bookId=123 | /books/123/comments |
| Consistent case | /UserProfile | /user-profile |
6.2 HTTP Methods and Status Codes
Guidelines// Always use appropriate methods
✓ GET for retrieval
✓ POST for creation
✓ PUT for full updates
✓ PATCH for partial updates
✓ DELETE for deletion
// Return correct status codes
201 Created → after POST (with location header)
200 OK → after successful GET, PUT, PATCH
204 No Content → after DELETE
400 Bad Request → validation errors
401 Unauthorized → missing/invalid auth
403 Forbidden → authenticated but not allowed
404 Not Found → resource doesn't exist
422 Unprocessable Entity → semantic errors
500 Internal Server Error → unexpected errors
6.3 Security Best Practices
JavaScript// 1. Validate all inputs with Joi
const Joi = require('joi');
const bookSchema = Joi.object({
title: Joi.string().required().max(100),
author: Joi.string().required(),
year: Joi.number().integer().min(1000).max(new Date().getFullYear()),
isbn: Joi.string().pattern(/^[0-9-]{10,17}$/)
});
// 2. Sanitize output to prevent XSS
const escapeHtml = require('escape-html');
// 3. Set security headers
const helmet = require('helmet');
app.use(helmet());
// 4. Rate limiting
const rateLimit = require('express-rate-limit');
app.use('/api/', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
// 5. Use HTTPS in production
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
}
6.4 Documentation
JavaScript — Swagger/OpenAPIconst swaggerUi = require('swagger-ui-express');
const swaggerDocument = {
openapi: '3.0.0',
info: {
title: 'Library API',
version: '1.0.0',
description: 'A simple library management API'
},
servers: [{ url: 'http://localhost:3000', description: 'Development server' }],
paths: {
'/api/books': {
get: {
summary: 'Returns all books',
parameters: [
{ name: 'author', in: 'query', schema: { type: 'string' } },
{ name: 'page', in: 'query', schema: { type: 'integer' } }
],
responses: { 200: { description: 'Successful response' } }
}
}
}
};
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
7. Conclusion
You've now learned the fundamentals of building REST APIs with Express.js:
REST API Basics
- REST uses standard HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Status codes communicate results (200 success, 404 not found, 500 error)
- Endpoints should use nouns, plural forms, and logical nesting
CRUD Operations
- CREATE (POST) — Add new resources
- READ (GET) — Retrieve resources (single or collection)
- UPDATE (PUT/PATCH) — Modify existing resources
- DELETE (DELETE) — Remove resources
Express Implementation
- Use
express.json() middleware for parsing JSON
- Implement proper validation and error handling
- Structure routes logically (separate files for larger apps)
Postman Testing
- Create collections to organize requests
- Use environments for different configurations
- Write tests to automate validation
- Save variables from responses for chained requests
Best Practices: Validate all inputs, use appropriate HTTP status codes, implement rate limiting and security headers, document your API, and version your API from the start.