← Back to Blog

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 Chen15 min readadvanced
S

Sarah Chen

Senior Software Engineer

Sarah 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.

TypeScriptAPI DevelopmentPythonData EngineeringJSON SchemaPerformance Tuning
15 min read

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

  • Use appropriate HTTP status codes (200, 201, 400, 401, 404, 500)
  • Version your API (/api/v1/users) for backward compatibility
  • Implement rate limiting to prevent abuse
  • Validate all input data before processing
  • Use HTTPS only in production
  • Implement CORS properly for browser security
  • Add request logging for debugging and monitoring
  • Return consistent error formats across all endpoints
  • Include pagination for list endpoints
  • Document with OpenAPI/Swagger for developer experience
  • 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.

  • Use HTTPS in production
  • Document your API (OpenAPI/Swagger)
  • 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!

    Share:

    Related Articles