About us Guides Projects Contacts
Админка
please wait

CouchDB is a document-oriented NoSQL database designed for reliability, horizontal scaling, and seamless synchronization. Its master-master replication makes it ideal for offline-first applications where data must sync across devices. This guide covers building sync-enabled applications with CouchDB from a senior developer's perspective.

Why CouchDB

CouchDB excels in specific scenarios:

  1. Master-Master Replication: Sync between any nodes bidirectionally
  2. Offline-First: PouchDB in the browser syncs when connected
  3. HTTP API: RESTful interface; no special drivers needed
  4. Conflict Resolution: Built-in handling for concurrent edits
  5. CouchApps: Serve HTML/JSON directly from the database

Best suited for:

  • Mobile apps requiring offline capability
  • Multi-region deployments needing data sync
  • High-velocity logging systems
  • Applications with intermittent connectivity

Installation

Using Docker

docker run -d --name couchdb \
-p 5984:5984 \
-e COUCHDB_USER=admin \
-e COUCHDB_PASSWORD=password \
couchdb:latest

Docker Compose

version: '3.8'
services:
couchdb:
image: couchdb:latest
ports:
- "5984:5984"
environment:
COUCHDB_USER: admin
COUCHDB_PASSWORD: password
volumes:
- couchdb_data:/opt/couchdb/data
volumes:
couchdb_data:

Linux Installation

# Ubuntu/Debian
apt-get install couchdb
# CentOS/RHEL
yum install couchdb

Access Fauxton (admin UI): http://localhost:5984/_utils/

CouchDB Basics

HTTP API

CouchDB uses a pure RESTful HTTP API:

# Check server
curl http://localhost:5984/
# Create database
curl -X PUT http://admin:password@localhost:5984/mydb
# List databases
curl http://admin:password@localhost:5984/_all_dbs
# Delete database
curl -X DELETE http://admin:password@localhost:5984/mydb

Document Operations

# Create document
curl -X POST http://admin:password@localhost:5984/mydb \
-H "Content-Type: application/json" \
-d '{"name": "John", "email": "[email protected]"}'
# Get document
curl http://localhost:5984/mydb/document_id
# Update document (must include _rev)
curl -X PUT http://admin:password@localhost:5984/mydb/document_id \
-H "Content-Type: application/json" \
-d '{"_rev": "1-xxx", "name": "John Updated"}'
# Delete document
curl -X DELETE http://admin:password@localhost:5984/mydb/document_id?rev=1-xxx

Node.js Integration

Using nano

npm install nano
const nano = require('nano')('http://admin:password@localhost:5984');
// Create database
async function setupDatabase() {
try {
await nano.db.create('myapp');
console.log('Database created');
} catch (err) {
if (err.statusCode !== 412) throw err; // Already exists
}
}
const db = nano.db.use('myapp');
// CRUD operations
async function createDocument(doc) {
const response = await db.insert(doc);
return { id: response.id, rev: response.rev };
}
async function getDocument(id) {
try {
return await db.get(id);
} catch (err) {
if (err.statusCode === 404) return null;
throw err;
}
}
async function updateDocument(id, updates) {
const doc = await db.get(id);
const updated = { ...doc, ...updates };
return db.insert(updated);
}
async function deleteDocument(id) {
const doc = await db.get(id);
return db.destroy(id, doc._rev);
}
// Bulk operations
async function bulkInsert(docs) {
return db.bulk({ docs });
}
async function getAllDocuments() {
const response = await db.list({ include_docs: true });
return response.rows.map(row => row.doc);
}

Views and Queries

Creating Views

Views are defined in design documents:

const designDoc = {
_id: '_design/app',
views: {
// Simple view: emit all documents by type
by_type: {
map: function(doc) {
if (doc.type) {
emit(doc.type, null);
}
}.toString()
},
// View with values
users_by_email: {
map: function(doc) {
if (doc.type === 'user') {
emit(doc.email, {
name: doc.name,
created: doc.createdAt
});
}
}.toString()
},
// Compound keys
posts_by_date: {
map: function(doc) {
if (doc.type === 'post') {
var date = new Date(doc.createdAt);
emit([date.getFullYear(), date.getMonth() + 1, date.getDate()], doc.title);
}
}.toString()
},
// With reduce for counting
count_by_type: {
map: function(doc) {
if (doc.type) {
emit(doc.type, 1);
}
}.toString(),
reduce: '_count'
}
}
};
await db.insert(designDoc);

Querying Views

// Get all of a type
const users = await db.view('app', 'by_type', {
key: 'user',
include_docs: true
});
// Range query
const posts = await db.view('app', 'posts_by_date', {
startkey: [2024, 1, 1],
endkey: [2024, 12, 31],
include_docs: true
});
// Pagination
const page = await db.view('app', 'users_by_email', {
limit: 10,
skip: 20,
include_docs: true
});
// Get count
const counts = await db.view('app', 'count_by_type', {
group: true
});

Mango Queries (CouchDB 2.0+)

More familiar SQL-like syntax:

// Create index
await db.createIndex({
index: { fields: ['type', 'createdAt'] },
name: 'type-date-index'
});
// Query with selector
const result = await db.find({
selector: {
type: 'user',
createdAt: { $gte: '2024-01-01' }
},
sort: [{ createdAt: 'desc' }],
limit: 10
});

Offline-First with PouchDB

PouchDB runs in browsers and syncs with CouchDB:

Browser Setup

<script src="https://cdn.jsdelivr.net/npm/pouchdb@8/dist/pouchdb.min.js"></script>
// Local database (IndexedDB in the browser)
const localDB = new PouchDB('myapp');
// Remote CouchDB
const remoteDB = new PouchDB('http://localhost:5984/myapp', {
auth: {
username: 'admin',
password: 'password'
}
});
// Continuous sync
const sync = localDB.sync(remoteDB, {
live: true,
retry: true
})
.on('change', info => {
console.log('Sync change:', info);
})
.on('paused', err => {
console.log('Sync paused', err ? 'with error' : 'up to date');
})
.on('active', () => {
console.log('Sync resumed');
})
.on('denied', err => {
console.error('Sync denied:', err);
})
.on('error', err => {
console.error('Sync error:', err);
});
// Stop sync when needed
// sync.cancel();

CRUD Operations

// Create
async function createItem(item) {
const doc = {
_id: new Date().toISOString(), // Or use PouchDB.uuid()
type: 'item',
...item,
createdAt: new Date().toISOString()
};
return localDB.put(doc);
}
// Read
async function getItem(id) {
try {
return await localDB.get(id);
} catch (err) {
if (err.status === 404) return null;
throw err;
}
}
// Update
async function updateItem(id, updates) {
const doc = await localDB.get(id);
return localDB.put({
...doc,
...updates,
updatedAt: new Date().toISOString()
});
}
// Delete
async function deleteItem(id) {
const doc = await localDB.get(id);
return localDB.remove(doc);
}
// List all
async function getAllItems() {
const result = await localDB.allDocs({
include_docs: true,
startkey: 'item_',
endkey: 'item_\ufff0'
});
return result.rows.map(row => row.doc);
}

React Integration

import { useState, useEffect } from 'react';
import PouchDB from 'pouchdb';
const db = new PouchDB('tasks');
function useTasks() {
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Initial load
loadTasks();
// Listen for changes
const changes = db.changes({
since: 'now',
live: true,
include_docs: true
}).on('change', () => {
loadTasks();
});
return () => changes.cancel();
}, []);
async function loadTasks() {
const result = await db.allDocs({ include_docs: true });
setTasks(result.rows.map(r => r.doc).filter(d => d.type === 'task'));
setLoading(false);
}
async function addTask(title) {
await db.put({
_id: `task_${Date.now()}`,
type: 'task',
title,
completed: false,
createdAt: new Date().toISOString()
});
}
async function toggleTask(task) {
await db.put({
...task,
completed: !task.completed
});
}
async function deleteTask(task) {
await db.remove(task);
}
return { tasks, loading, addTask, toggleTask, deleteTask };
}
function TaskList() {
const { tasks, loading, addTask, toggleTask, deleteTask } = useTasks();
const [newTask, setNewTask] = useState('');
if (loading) return <div>Loading...</div>;
return (
<div>
<form onSubmit={e => {
e.preventDefault();
addTask(newTask);
setNewTask('');
}}>
<input
value={newTask}
onChange={e => setNewTask(e.target.value)}
placeholder="New task"
/>
<button type="submit">Add</button>
</form>
<ul>
{tasks.map(task => (
<li key={task._id}>
<input
type="checkbox"
checked={task.completed}
onChange={() => toggleTask(task)}
/>
<span style={{ textDecoration: task.completed ? 'line-through' : 'none' }}>
{task.title}
</span>
<button onClick={() => deleteTask(task)}>Delete</button>
</li>
))}
</ul>
</div>
);
}

Conflict Resolution

When the same document is edited on multiple devices:

// Get document with conflicts
const doc = await db.get(docId, { conflicts: true });
if (doc._conflicts) {
// Get conflicting revisions
const conflicts = await Promise.all(
doc._conflicts.map(rev => db.get(docId, { rev }))
);
// Resolve: merge or pick a winner
const resolved = mergeConflicts(doc, conflicts);
// Save resolved version
await db.put(resolved);
// Delete losing revisions
for (const conflict of doc._conflicts) {
await db.remove(docId, conflict);
}
}
function mergeConflicts(winner, losers) {
// Example: merge arrays, keep latest scalars
const merged = { ...winner };
for (const loser of losers) {
// Merge tags arrays
if (loser.tags) {
merged.tags = [...new Set([...(merged.tags || []), ...loser.tags])];
}
// Keep most recent update
if (loser.updatedAt > merged.updatedAt) {
merged.content = loser.content;
merged.updatedAt = loser.updatedAt;
}
}
return merged;
}

Key Takeaways

  1. Document model: Think in JSON documents, not tables
  2. Design for sync: Plan ID schemes and conflict resolution upfront
  3. Views for queries: Create views for common access patterns
  4. PouchDB for offline: Seamless browser-to-server sync
  5. Eventual consistency: Embrace it; don't fight it
  6. Conflict handling: Always have a strategy

CouchDB shines for applications that need reliable data sync across devices and regions—embrace its strengths rather than forcing relational patterns.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive