Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

A JSON file as storage

While the blog has no database, posts can be kept in an ordinary JSON file. The program reads the file, turns the text into an array of objects, changes the array and writes it back. For a learning project or a small utility this is quite enough.

Saving and reading

An object becomes text with JSON.stringify(), and text becomes an object with JSON.parse():

import { readFile, writeFile } from 'node:fs/promises';
 
const posts = [{ id: 1, title: 'Hello, Node.js', body: 'The first post.' }];
await writeFile('posts.json', JSON.stringify(posts, null, 2));
 
const saved = JSON.parse(await readFile('posts.json', 'utf8'));
 
console.log(saved[0].title); // Hello, Node.js

The null, 2 arguments add two-space indentation — the file is easy to read and edit by hand.

When the file does not exist yet

On the first run there is no file. In that case return an empty array and rethrow any other error:

import { readFile } from 'node:fs/promises';
 
async function loadPosts(file) {
  try {
    return JSON.parse(await readFile(file, 'utf8'));
  } catch (error) {
    if (error.code === 'ENOENT') return [];
    throw error;
  }
}
 
console.log(await loadPosts('no-such-file.json')); // []

The storage module

Put everything into the module src/posts.js — the blog server will use it. The data file lives in data/posts.json:

// src/posts.js
import path from 'node:path';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
 
const DATA_FILE = path.join(import.meta.dirname, '..', 'data', 'posts.json');
 
export async function loadPosts() {
  try {
    return JSON.parse(await readFile(DATA_FILE, 'utf8'));
  } catch (error) {
    if (error.code === 'ENOENT') return [];
    throw error;
  }
}
 
async function savePosts(posts) {
  await mkdir(path.dirname(DATA_FILE), { recursive: true });
  await writeFile(DATA_FILE, JSON.stringify(posts, null, 2));
}
 
export async function addPost(title, body) {
  const posts = await loadPosts();
  const id = posts.length === 0 ? 1 : Math.max(...posts.map((p) => p.id)) + 1;
  const post = { id, title, body, createdAt: new Date().toISOString() };
  posts.push(post);
  await savePosts(posts);
  return post;
}
 
export async function findPost(id) {
  const posts = await loadPosts();
  return posts.find((post) => post.id === id);
}

A new post gets a number one greater than the current maximum, and the date is stored as a string.

Using it

Create the file add-post.js in the root; it adds a post from the command-line arguments:

// add-post.js
import { addPost, loadPosts } from './src/posts.js';
 
const [title = 'Untitled', body = ''] = process.argv.slice(2);
const post = await addPost(title, body);
 
console.log(`Added post #${post.id}`);
console.log(`Total posts: ${(await loadPosts()).length}`);

Run node add-post.js "Hello, Node.js" "The first post" twice — the numbers are 1 and 2, and both posts appear in data/posts.json.

Common mistake

writeFile() accepts only strings and bytes: an object without JSON.stringify() throws ERR_INVALID_ARG_TYPE. And if the object is turned into a string carelessly — with String() or a template string — the file gets [object Object], and the next JSON.parse() fails:

import { readFile, writeFile } from 'node:fs/promises';
 
await writeFile('broken.json', String({ id: 1 }));
 
console.log(await readFile('broken.json', 'utf8')); // [object Object]

Always save JSON.stringify(data) and read with JSON.parse(text).

Thanks for reading!

Donate to the site — we work hard for you.