APIs and Integration
Overview
Node.js excels at building APIs and integrating with external services. This chapter covers REST API development, GraphQL implementation, third-party service integration, authentication, and best practices for API design and consumption.
REST API Development
Basic REST API with Express
javascript
// rest-api-basic.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const app = express();
// Security and middleware
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: {
error: 'Too many requests from this IP',
retryAfter: '15 minutes'
}
});
app.use('/api/', limiter);
// Mock database
let users = [
{ id: 1, name: 'John Doe', email: 'john@example.com', createdAt: new Date() },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', createdAt: new Date() }
];
let nextId = 3;
// Validation middleware
const validateUser = (req, res, next) => {
const { name, email } = req.body;
const errors = [];
if (!name || typeof name !== 'string' || name.trim().length === 0) {
errors.push('Name is required and must be a non-empty string');
}
if (!email || typeof email !== 'string' || !email.includes('@')) {
errors.push('Valid email is required');
}
if (errors.length > 0) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors
});
}
next();
};
// GET /api/users - Get all users with pagination
app.get('/api/users', (req, res) => {
const { page = 1, limit = 10, search, sortBy = 'id', sortOrder = 'asc' } = req.query;
let filteredUsers = [...users];
// Search functionality
if (search) {
const searchLower = search.toLowerCase();
filteredUsers = users.filter(user =>
user.name.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower)
);
}
// Sorting
filteredUsers.sort((a, b) => {
const aValue = a[sortBy];
const bValue = b[sortBy];
if (sortOrder === 'desc') {
return aValue < bValue ? 1 : -1;
}
return aValue > bValue ? 1 : -1;
});
// Pagination
const startIndex = (page - 1) * limit;
const endIndex = startIndex + parseInt(limit);
const paginatedUsers = filteredUsers.slice(startIndex, endIndex);
res.json({
success: true,
data: paginatedUsers,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: filteredUsers.length,
pages: Math.ceil(filteredUsers.length / limit),
hasNext: endIndex < filteredUsers.length,
hasPrev: page > 1
}
});
});
// GET /api/users/:id - Get user by ID
app.get('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
data: user
});
});
// POST /api/users - Create new user
app.post('/api/users', validateUser, (req, res) => {
const { name, email } = req.body;
// Check for duplicate email
const existingUser = users.find(u => u.email === email);
if (existingUser) {
return res.status(409).json({
success: false,
message: 'User with this email already exists'
});
}
const newUser = {
id: nextId++,
name: name.trim(),
email: email.trim().toLowerCase(),
createdAt: new Date(),
updatedAt: new Date()
};
users.push(newUser);
res.status(201).json({
success: true,
message: 'User created successfully',
data: newUser
});
});
// PUT /api/users/:id - Update user
app.put('/api/users/:id', validateUser, (req, res) => {
const id = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
const { name, email } = req.body;
// Check for duplicate email (excluding current user)
const existingUser = users.find(u => u.email === email && u.id !== id);
if (existingUser) {
return res.status(409).json({
success: false,
message: 'Another user with this email already exists'
});
}
users[userIndex] = {
...users[userIndex],
name: name.trim(),
email: email.trim().toLowerCase(),
updatedAt: new Date()
};
res.json({
success: true,
message: 'User updated successfully',
data: users[userIndex]
});
});
// DELETE /api/users/:id - Delete user
app.delete('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
const deletedUser = users.splice(userIndex, 1)[0];
res.json({
success: true,
message: 'User deleted successfully',
data: deletedUser
});
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage()
});
});
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({
success: false,
message: 'Endpoint not found',
path: req.originalUrl
});
});
// Error handler
app.use((error, req, res, next) => {
console.error('API Error:', error);
res.status(error.status || 500).json({
success: false,
message: error.message || 'Internal server error',
...(process.env.NODE_ENV === 'development' && { stack: error.stack })
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`REST API server running on http://localhost:${PORT}`);
});Advanced REST API Features
javascript
// rest-api-advanced.js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const multer = require('multer');
const path = require('path');
const app = express();
app.use(express.json());
// JWT Authentication middleware
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET || 'secret', (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.user = user;
next();
});
};
// Role-based authorization
const authorize = (roles = []) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (roles.length && !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
// File upload configuration
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB limit
},
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif|pdf/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('Invalid file type'));
}
}
});
// API versioning
const v1Router = express.Router();
const v2Router = express.Router();
// Version 1 endpoints
v1Router.get('/users', (req, res) => {
res.json({
version: 'v1',
users: [{ id: 1, name: 'John' }]
});
});
// Version 2 endpoints (enhanced response format)
v2Router.get('/users', (req, res) => {
res.json({
version: 'v2',
data: {
users: [{
id: 1,
firstName: 'John',
lastName: 'Doe',
profile: { email: 'john@example.com' }
}],
meta: {
total: 1,
page: 1,
limit: 10
}
}
});
});
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Content negotiation
app.get('/api/data', (req, res) => {
const data = { message: 'Hello World', timestamp: new Date() };
res.format({
'application/json': () => {
res.json(data);
},
'application/xml': () => {
const xml = `<?xml version="1.0"?>
<response>
<message>${data.message}</message>
<timestamp>${data.timestamp}</timestamp>
</response>`;
res.type('application/xml').send(xml);
},
'text/plain': () => {
res.send(`${data.message} - ${data.timestamp}`);
},
default: () => {
res.status(406).json({ error: 'Not Acceptable' });
}
});
});
// File upload endpoint
app.post('/api/upload', authenticateToken, upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
res.json({
success: true,
message: 'File uploaded successfully',
file: {
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
mimetype: req.file.mimetype,
path: req.file.path
}
});
});
// Batch operations
app.post('/api/users/batch', authenticateToken, authorize(['admin']), (req, res) => {
const { operations } = req.body;
if (!Array.isArray(operations)) {
return res.status(400).json({ error: 'Operations must be an array' });
}
const results = operations.map((operation, index) => {
try {
switch (operation.type) {
case 'create':
return { index, success: true, data: { id: Date.now() + index, ...operation.data } };
case 'update':
return { index, success: true, data: { id: operation.id, ...operation.data } };
case 'delete':
return { index, success: true, message: `User ${operation.id} deleted` };
default:
return { index, success: false, error: 'Unknown operation type' };
}
} catch (error) {
return { index, success: false, error: error.message };
}
});
res.json({
success: true,
results,
summary: {
total: operations.length,
successful: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length
}
});
});
// Server-Sent Events (SSE)
app.get('/api/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
// Send initial event
res.write(`data: ${JSON.stringify({ type: 'connected', timestamp: new Date() })}\n\n`);
// Send periodic updates
const interval = setInterval(() => {
const event = {
type: 'update',
data: { value: Math.random(), timestamp: new Date() }
};
res.write(`data: ${JSON.stringify(event)}\n\n`);
}, 5000);
// Clean up on client disconnect
req.on('close', () => {
clearInterval(interval);
res.end();
});
});
app.listen(3000);GraphQL Implementation
Basic GraphQL Server
javascript
// graphql-server.js
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
// GraphQL schema
const schema = buildSchema(`
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
published: Boolean!
createdAt: String!
}
input UserInput {
name: String!
email: String!
}
input PostInput {
title: String!
content: String!
authorId: ID!
published: Boolean = false
}
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
post(id: ID!): Post
searchUsers(query: String!): [User!]!
}
type Mutation {
createUser(input: UserInput!): User!
updateUser(id: ID!, input: UserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: PostInput!): Post!
updatePost(id: ID!, input: PostInput!): Post!
deletePost(id: ID!): Boolean!
}
type Subscription {
userAdded: User!
postAdded: Post!
}
`);
// Mock data
let users = [
{ id: '1', name: 'John Doe', email: 'john@example.com', createdAt: new Date().toISOString() },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', createdAt: new Date().toISOString() }
];
let posts = [
{ id: '1', title: 'First Post', content: 'Hello World', authorId: '1', published: true, createdAt: new Date().toISOString() },
{ id: '2', title: 'Second Post', content: 'GraphQL is awesome', authorId: '2', published: false, createdAt: new Date().toISOString() }
];
let nextUserId = 3;
let nextPostId = 3;
// Resolvers
const root = {
// Queries
users: () => users,
user: ({ id }) => users.find(user => user.id === id),
posts: () => posts.map(post => ({
...post,
author: users.find(user => user.id === post.authorId)
})),
post: ({ id }) => {
const post = posts.find(post => post.id === id);
if (post) {
return {
...post,
author: users.find(user => user.id === post.authorId)
};
}
return null;
},
searchUsers: ({ query }) => {
const searchTerm = query.toLowerCase();
return users.filter(user =>
user.name.toLowerCase().includes(searchTerm) ||
user.email.toLowerCase().includes(searchTerm)
);
},
// Mutations
createUser: ({ input }) => {
const newUser = {
id: String(nextUserId++),
name: input.name,
email: input.email,
createdAt: new Date().toISOString()
};
users.push(newUser);
return newUser;
},
updateUser: ({ id, input }) => {
const userIndex = users.findIndex(user => user.id === id);
if (userIndex === -1) {
throw new Error('User not found');
}
users[userIndex] = {
...users[userIndex],
...input
};
return users[userIndex];
},
deleteUser: ({ id }) => {
const userIndex = users.findIndex(user => user.id === id);
if (userIndex === -1) {
return false;
}
users.splice(userIndex, 1);
// Also delete user's posts
posts = posts.filter(post => post.authorId !== id);
return true;
},
createPost: ({ input }) => {
const author = users.find(user => user.id === input.authorId);
if (!author) {
throw new Error('Author not found');
}
const newPost = {
id: String(nextPostId++),
title: input.title,
content: input.content,
authorId: input.authorId,
published: input.published,
createdAt: new Date().toISOString()
};
posts.push(newPost);
return {
...newPost,
author
};
},
updatePost: ({ id, input }) => {
const postIndex = posts.findIndex(post => post.id === id);
if (postIndex === -1) {
throw new Error('Post not found');
}
posts[postIndex] = {
...posts[postIndex],
...input
};
const author = users.find(user => user.id === posts[postIndex].authorId);
return {
...posts[postIndex],
author
};
},
deletePost: ({ id }) => {
const postIndex = posts.findIndex(post => post.id === id);
if (postIndex === -1) {
return false;
}
posts.splice(postIndex, 1);
return true;
}
};
// Add posts field to User type
const originalUserResolver = root.user;
root.user = ({ id }) => {
const user = users.find(user => user.id === id);
if (user) {
return {
...user,
posts: posts.filter(post => post.authorId === id).map(post => ({
...post,
author: user
}))
};
}
return null;
};
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true, // Enable GraphiQL interface
}));
app.listen(4000, () => {
console.log('GraphQL server running on http://localhost:4000/graphql');
});Third-Party API Integration
HTTP Client Wrapper
javascript
// api-client.js
const axios = require('axios');
class APIClient {
constructor(baseURL, options = {}) {
this.client = axios.create({
baseURL,
timeout: options.timeout || 10000,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
this.retryConfig = {
retries: options.retries || 3,
retryDelay: options.retryDelay || 1000,
retryCondition: options.retryCondition || this.defaultRetryCondition
};
this.setupInterceptors();
}
setupInterceptors() {
// Request interceptor
this.client.interceptors.request.use(
(config) => {
console.log(`Making ${config.method.toUpperCase()} request to ${config.url}`);
return config;
},
(error) => {
console.error('Request error:', error);
return Promise.reject(error);
}
);
// Response interceptor
this.client.interceptors.response.use(
(response) => {
console.log(`Response received: ${response.status} ${response.statusText}`);
return response;
},
async (error) => {
const originalRequest = error.config;
if (this.shouldRetry(error) && !originalRequest._retry) {
originalRequest._retry = true;
originalRequest._retryCount = (originalRequest._retryCount || 0) + 1;
if (originalRequest._retryCount <= this.retryConfig.retries) {
console.log(`Retrying request (${originalRequest._retryCount}/${this.retryConfig.retries})`);
await this.delay(this.retryConfig.retryDelay * originalRequest._retryCount);
return this.client(originalRequest);
}
}
return Promise.reject(error);
}
);
}
defaultRetryCondition(error) {
return (
error.code === 'ECONNABORTED' ||
error.code === 'ENOTFOUND' ||
error.code === 'ECONNRESET' ||
(error.response && error.response.status >= 500)
);
}
shouldRetry(error) {
return this.retryConfig.retryCondition(error);
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Authentication methods
setAuthToken(token) {
this.client.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
setApiKey(key, headerName = 'X-API-Key') {
this.client.defaults.headers.common[headerName] = key;
}
// HTTP methods
async get(url, config = {}) {
try {
const response = await this.client.get(url, config);
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
async post(url, data, config = {}) {
try {
const response = await this.client.post(url, data, config);
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
async put(url, data, config = {}) {
try {
const response = await this.client.put(url, data, config);
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
async delete(url, config = {}) {
try {
const response = await this.client.delete(url, config);
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
handleError(error) {
if (error.response) {
// Server responded with error status
const apiError = new Error(error.response.data?.message || error.message);
apiError.status = error.response.status;
apiError.data = error.response.data;
return apiError;
} else if (error.request) {
// Request was made but no response received
const networkError = new Error('Network error: No response received');
networkError.code = 'NETWORK_ERROR';
return networkError;
} else {
// Something else happened
return error;
}
}
}
// Service-specific API clients
class GitHubAPI extends APIClient {
constructor(token) {
super('https://api.github.com', {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github.v3+json'
}
});
}
async getUser(username) {
return this.get(`/users/${username}`);
}
async getUserRepos(username) {
return this.get(`/users/${username}/repos`);
}
async createRepo(data) {
return this.post('/user/repos', data);
}
}
class StripeAPI extends APIClient {
constructor(secretKey) {
super('https://api.stripe.com/v1', {
headers: {
'Authorization': `Bearer ${secretKey}`
}
});
}
async createCustomer(data) {
return this.post('/customers', data);
}
async createPaymentIntent(data) {
return this.post('/payment_intents', data);
}
async getCustomer(customerId) {
return this.get(`/customers/${customerId}`);
}
}
// Usage examples
async function demonstrateAPIIntegration() {
// GitHub API example
const github = new GitHubAPI(process.env.GITHUB_TOKEN);
try {
const user = await github.getUser('octocat');
console.log('GitHub user:', user.name);
const repos = await github.getUserRepos('octocat');
console.log('Repository count:', repos.length);
} catch (error) {
console.error('GitHub API error:', error.message);
}
// Generic API client example
const jsonPlaceholder = new APIClient('https://jsonplaceholder.typicode.com');
try {
const posts = await jsonPlaceholder.get('/posts');
console.log('Posts count:', posts.length);
const newPost = await jsonPlaceholder.post('/posts', {
title: 'Test Post',
body: 'This is a test post',
userId: 1
});
console.log('Created post:', newPost.id);
} catch (error) {
console.error('JSONPlaceholder API error:', error.message);
}
}
module.exports = { APIClient, GitHubAPI, StripeAPI };Webhook Handling
javascript
// webhook-handler.js
const express = require('express');
const crypto = require('crypto');
const bodyParser = require('body-parser');
class WebhookHandler {
constructor() {
this.handlers = new Map();
this.middleware = [];
}
// Register webhook handler
register(event, handler) {
if (!this.handlers.has(event)) {
this.handlers.set(event, []);
}
this.handlers.get(event).push(handler);
}
// Add middleware
use(middleware) {
this.middleware.push(middleware);
}
// Verify webhook signature
verifySignature(payload, signature, secret, algorithm = 'sha256') {
const expectedSignature = crypto
.createHmac(algorithm, secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Process webhook
async process(event, payload, headers = {}) {
// Run middleware
for (const middleware of this.middleware) {
await middleware(event, payload, headers);
}
// Get handlers for event
const handlers = this.handlers.get(event) || [];
// Execute all handlers
const results = await Promise.allSettled(
handlers.map(handler => handler(payload, headers))
);
// Log any failures
results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(`Handler ${index} failed for event ${event}:`, result.reason);
}
});
return results;
}
// Create Express middleware
createExpressHandler(options = {}) {
const {
path = '/webhook',
secret,
signatureHeader = 'x-signature',
eventHeader = 'x-event-type'
} = options;
const router = express.Router();
// Raw body parser for signature verification
router.use(path, bodyParser.raw({ type: 'application/json' }));
router.post(path, async (req, res) => {
try {
const payload = req.body;
const signature = req.headers[signatureHeader];
const event = req.headers[eventHeader];
if (!event) {
return res.status(400).json({ error: 'Missing event type header' });
}
// Verify signature if secret is provided
if (secret && signature) {
if (!this.verifySignature(payload, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
}
// Parse JSON payload
const parsedPayload = JSON.parse(payload.toString());
// Process webhook
await this.process(event, parsedPayload, req.headers);
res.status(200).json({ success: true, message: 'Webhook processed' });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Webhook processing failed' });
}
});
return router;
}
}
// Usage example
const webhookHandler = new WebhookHandler();
// Add logging middleware
webhookHandler.use(async (event, payload, headers) => {
console.log(`Received webhook: ${event}`, {
timestamp: new Date().toISOString(),
payloadSize: JSON.stringify(payload).length,
userAgent: headers['user-agent']
});
});
// Register event handlers
webhookHandler.register('user.created', async (payload) => {
console.log('New user created:', payload.user.email);
// Send welcome email
// Update analytics
});
webhookHandler.register('payment.completed', async (payload) => {
console.log('Payment completed:', payload.payment.id);
// Update order status
// Send confirmation email
});
webhookHandler.register('order.shipped', async (payload) => {
console.log('Order shipped:', payload.order.id);
// Send tracking information
// Update inventory
});
// Create Express app
const app = express();
// Mount webhook handler
app.use(webhookHandler.createExpressHandler({
path: '/webhooks',
secret: process.env.WEBHOOK_SECRET,
signatureHeader: 'x-hub-signature-256',
eventHeader: 'x-github-event'
}));
app.listen(3000, () => {
console.log('Webhook server running on http://localhost:3000');
});
module.exports = WebhookHandler;Next Steps
In the next chapter, we'll explore error handling strategies and best practices for building robust Node.js applications.
Practice Exercises
- Build a complete REST API with authentication, authorization, and file uploads
- Create a GraphQL API with subscriptions using WebSockets
- Implement a webhook system for processing GitHub events
- Build an API gateway that aggregates multiple microservices
Key Takeaways
- REST APIs provide a standardized way to expose application functionality
- GraphQL offers flexible data querying and real-time subscriptions
- Proper authentication and authorization are essential for API security
- HTTP client wrappers simplify third-party API integration
- Webhook handlers enable real-time event processing
- API versioning ensures backward compatibility
- Rate limiting and validation protect against abuse
- Comprehensive error handling improves API reliability