JSON APIs and REST Services: Complete Development Guide
Learn to build and consume JSON-based REST APIs. Covers HTTP methods, authentication, best practices, and real-world implementation examples.
Sarah Chen
• Senior Software EngineerSarah is a full-stack software engineer with 8 years of experience in API development, TypeScript, and data engineering. She has designed and maintained large-scale JSON processing pipelines and contributes in-depth technical guides on performance optimisation, schema design, Python data workflows, and backend integration patterns.
REST API Fundamentals
REST (Representational State Transfer) APIs use JSON as the primary data format for request and response bodies.
HTTP Methods
- GET: Retrieve data
- POST: Create new resource
- PUT: Update entire resource
- PATCH: Partial update
- DELETE: Remove resource
Building APIs with Express.js
import express from 'express';
const app = express();
app.use(express.json());
let users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
// GET all users
app.get('/api/users', (req, res) => {
res.json(users);
});
// GET single user
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
// POST create user
app.post('/api/users', (req, res) => {
const user = { id: users.length + 1, ...req.body };
users.push(user);
res.status(201).json(user);
});
app.listen(3000);
Building APIs with Python FastAPI
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
name: str
email: str
users_db = []
@app.get("/api/users")
def get_users():
return users_db
@app.post("/api/users", status_code=201)
def create_user(user: User):
users_db.append(user.dict())
return user
@app.get("/api/users/{user_id}")
def get_user(user_id: int):
if user_id >= len(users_db):
raise HTTPException(status_code=404, detail="Not found")
return users_db[user_id]
Consuming APIs
JavaScript Fetch
// GET request
const response = await fetch('https://api.example.com/users');
const users = await response.json();
// POST request
const newUser = await fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' })
});
Python Requests
import requests
# GET
response = requests.get('https://api.example.com/users')
users = response.json()
# POST
new_user = requests.post(
'https://api.example.com/users',
json={'name': 'Alice', 'email': 'alice@example.com'}
)
API Response Patterns
Success Response
{
"success": true,
"data": {
"id": 1,
"name": "Alice"
}
}
Error Response
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{
"field": "email",
"message": "Invalid email format"
}
]
}
}
Authentication
Bearer Token
fetch('/api/protected', {
headers: {
'Authorization': 'Bearer your-token-here'
}
});
API Key
fetch('/api/data', {
headers: {
'X-API-Key': 'your-api-key'
}
});
Pagination
{
"data": [...],
"pagination": {
"page": 1,
"perPage": 20,
"total": 156,
"totalPages": 8
}
}
Best Practices
Production API Patterns
Input Validation Middleware
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().optional()
});
function validateBody(schema) {
return (req, res, next) => {
try {
req.body = schema.parse(req.body);
next();
} catch (error) {
res.status(400).json({
error: 'Validation failed',
details: error.errors
});
}
};
}
app.post('/api/users', validateBody(CreateUserSchema), async (req, res) => {
const user = await db.users.create(req.body);
res.status(201).json(user);
});
Rate Limiting
import rateLimit from 'express-rate-limit';
const apiLimiter = rateLimit({
windowMs: 15 60 1000, // 15 minutes
max: 100, // 100 requests per window
message: { error: 'Too many requests, please try again later' },
standardHeaders: true,
legacyHeaders: false
});
app.use('/api/', apiLimiter);
Authentication Middleware
import jwt from 'jsonwebtoken';
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (error) {
res.status(401).json({ error: 'Invalid or expired token' });
}
}
app.get('/api/profile', authenticate, async (req, res) => {
const user = await db.users.findById(req.user.id);
res.json(user);
});
CORS Configuration
import cors from 'cors';
const corsOptions = {
origin: process.env.NODE_ENV === 'production'
? 'https://yourdomain.com'
: 'http://localhost:3000',
credentials: true,
optionsSuccessStatus: 200
};
app.use(cors(corsOptions));
Error Handling
// Global error handler
app.use((err, req, res, next) => {
console.error(err.stack);
// Operational error (safe to expose)
if (err.isOperational) {
return res.status(err.statusCode || 500).json({
error: err.message,
code: err.code
});
}
// Programming error (hide details)
res.status(500).json({
error: 'Internal server error'
});
});
Request Logging
import morgan from 'morgan';
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
app.use(morgan('combined', { stream: logger.stream }));
// Custom request logging
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info({
method: req.method,
url: req.url,
status: res.statusCode,
duration: Date.now() - start,
userId: req.user?.id
});
});
next();
});
Advanced Patterns
Cursor-Based Pagination
app.get('/api/posts', async (req, res) => {
const { cursor, limit = 20 } = req.query;
let query = db('posts')
.orderBy('created_at', 'desc')
.limit(limit + 1);
if (cursor) {
const decodedCursor = JSON.parse(Buffer.from(cursor, 'base64').toString());
query = query.where('created_at', '<', decodedCursor.created_at);
}
const posts = await query;
const hasMore = posts.length > limit;
const items = posts.slice(0, limit);
const nextCursor = hasMore
? Buffer.from(JSON.stringify({
created_at: items[items.length - 1].created_at
})).toString('base64')
: null;
res.json({
data: items,
pagination: {
cursor: nextCursor,
hasMore
}
});
});
Field Filtering (Sparse Fieldsets)
app.get('/api/users', async (req, res) => {
const { fields } = req.query;
let query = db('users');
if (fields) {
const selectedFields = fields.split(',');
query = query.select(selectedFields);
}
const users = await query;
res.json(users);
});
// Usage: GET /api/users?fields=id,name,email
Batch Operations
app.post('/api/users/batch', async (req, res) => {
const { operations } = req.body;
const results = await Promise.allSettled(
operations.map(async (op) => {
switch (op.type) {
case 'create':
return await db.users.create(op.data);
case 'update':
return await db.users.update(op.id, op.data);
case 'delete':
return await db.users.delete(op.id);
default:
throw new Error('Invalid operation');
}
})
);
res.json({
results: results.map((r, i) => ({
operation: operations[i].type,
success: r.status === 'fulfilled',
data: r.status === 'fulfilled' ? r.value : null,
error: r.status === 'rejected' ? r.reason.message : null
}))
});
});
API Versioning Strategy
// v1 routes
const v1Router = express.Router();
v1Router.get('/users', (req, res) => {
res.json({ version: 'v1', users: [] });
});
// v2 routes with breaking changes
const v2Router = express.Router();
v2Router.get('/users', (req, res) => {
res.json({ version: 'v2', data: { users: [] } });
});
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
Testing REST APIs
Unit Testing with Jest
import request from 'supertest';
import app from './app';
describe('Users API', () => {
it('should create a user', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Alice', email: 'alice@example.com' })
.expect(201)
.expect('Content-Type', /json/);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe('Alice');
});
it('should return 400 for invalid data', async () => {
await request(app)
.post('/api/users')
.send({ name: '' }) // Invalid
.expect(400);
});
});
Integration Testing
describe('Authentication Flow', () => {
it('should login and access protected route', async () => {
// 1. Register
await request(app)
.post('/api/register')
.send({ email: 'test@example.com', password: 'password123' });
// 2. Login
const loginRes = await request(app)
.post('/api/login')
.send({ email: 'test@example.com', password: 'password123' });
const token = loginRes.body.token;
// 3. Access protected route
const profileRes = await request(app)
.get('/api/profile')
.set('Authorization', Bearer ${token})
.expect(200);
expect(profileRes.body.email).toBe('test@example.com');
});
});
OpenAPI Documentation
# openapi.yaml
openapi: 3.0.0
info:
title: Users API
version: 1.0.0
paths:
/api/users:
get:
summary: List users
parameters:
- name: page
in: query
schema:
type: integer
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
Performance Optimization
Response Compression
import compression from 'compression';
app.use(compression()); // gzip compression (60-70% size reduction)
Caching with Redis
import Redis from 'ioredis';
const redis = new Redis();
app.get('/api/users/:id', async (req, res) => {
const cacheKey = user:${req.params.id};
// Check cache first
const cached = await redis.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached));
}
// Fetch from database
const user = await db.users.findById(req.params.id);
// Cache for 5 minutes
await redis.setex(cacheKey, 300, JSON.stringify(user));
res.json(user);
});
Database Query Optimization
// Select only needed fields
app.get('/api/users', async (req, res) => {
const users = await db('users')
.select('id', 'name', 'email') // Don't fetch all columns
.limit(100);
res.json(users);
});
// Use database indexes
// CREATE INDEX idx_email ON users(email);
Conclusion
Building production-ready REST APIs requires proper validation (Zod/JSON Schema), authentication (JWT), rate limiting, error handling, and pagination. Use cursor-based pagination for better performance, compress responses to save bandwidth, cache with Redis for frequently accessed data, and document with OpenAPI for great developer experience. Always test thoroughly with integration tests and monitor API performance in production.
Status Codes
- 200: OK
- 201: Created
- 204: No Content
- 400: Bad Request
- 401: Unauthorized
- 404: Not Found
- 500: Internal Server Error
Conclusion
REST APIs with JSON are the backbone of modern web applications. Master HTTP methods, proper status codes, and authentication for building robust APIs!
Related Articles
What is JSON? Complete Guide for Beginners 2026
Learn what JSON is, its syntax, data types, and use cases. A comprehensive beginner-friendly guide to understanding JavaScript Object Notation.
JavaScript JSON: Parse, Stringify, and Best Practices
Complete guide to JSON in JavaScript. Learn JSON.parse(), JSON.stringify(), error handling, and advanced techniques for web development.
Python and JSON: Complete Guide to json Module
Master JSON in Python with the json module. Learn to parse, generate, and manipulate JSON data with practical examples and best practices.