๐Ÿงฉ Day 7 โ€” Refactor With a Layered Structure

17 min readยทJan 1, 2026

Welcome to Day 7.

In yesterday's lesson, you added write support to the API and introduced middleware to validate requests.

In today's lesson, you'll refactor the project so the API stays clean and understandable as it grows.

You'll learn a simple way to separate responsibilities: keeping HTTP concerns in one place and business logic in another, without over-engineering.

By the end of this lesson, your API will behave exactly the same, but the codebase will be organized in a way that's easier to maintain and extend.

Implement services

Within the src/api directory, let's create a new directory named services that will contain the functions responsible for executing the logic previously written in the try block of the request handler functions of the HTTP GET /notes and HTTP POST /note endpoints.

~/projects/
โ””โ”€ lb_notes/
   โ”œโ”€ .env
   โ”œโ”€ bin/
   โ”œโ”€ package.json
   โ””โ”€ src/
      โ”œโ”€ api/
      โ”‚  โ”œโ”€ middlewares/
      โ”‚  โ”œโ”€ services/
      โ”‚  โ””โ”€ server.js
      โ”œโ”€ ...

Implement the listNotes service

Within the services directory, let's create a new file named list_notes.js.

~/projects/
โ””โ”€ lb_notes/
   โ”œโ”€ .env
   โ”œโ”€ bin/
   โ”œโ”€ package.json
   โ””โ”€ src/
      โ”œโ”€ api/
      โ”‚  โ”œโ”€ middlewares/
      โ”‚  โ”œโ”€ services/
      โ”‚  โ”‚  โ””โ”€ list_notes.js
      โ”‚  โ””โ”€ server.js
      โ”œโ”€ ...

Within this file, let's export a listNotes() function that implements the logic of the HTTP GET /notes endpoint.

src/api/services/list_notes.js
import { readJSON } from '../../storage/json_fs.js';

export default function listNotes(filePath) {
  const data = readJSON(filePath);

  if (data === null) {
    return { notes: [] };
  }
  else if (data?.notes === undefined || !Array.isArray(data?.notes)) {
    throw new Error('Invalid JSON file');
  }
  return data;
}

Implement the addNote service

Within the services directory, let's create a new file named add_note.js.

~/projects/
โ””โ”€ lb_notes/
   โ”œโ”€ .env
   โ”œโ”€ bin/
   โ”œโ”€ package.json
   โ””โ”€ src/
      โ”œโ”€ api/
      โ”‚  โ”œโ”€ middlewares/
      โ”‚  โ”œโ”€ services/
      โ”‚  โ”‚  โ”œโ”€ add_note.js
      โ”‚  โ”‚  โ””โ”€ list_notes.js
      โ”‚  โ””โ”€ server.js
      โ”œโ”€ ...

Within this file, let's export an addNote() function that implements the logic of the HTTP POST /note endpoint.

src/api/services/add_note.js
import { readJSON, writeJSON } from '../../storage/json_fs.js';

export default function addNote(filePath, note) {
  let data = readJSON(filePath);

  if (data === null) {
    data = { notes: [] };
  }
  else if (data?.notes === undefined || !Array.isArray(data?.notes)) {
    throw new Error('Invalid JSON file');
  }

  data.notes.push(note);
  writeJSON(filePath, data);
  return;
}

Implement controllers

Within the src/api directory, let's create a new directory named controllers that will contains the request handler functions for the HTTP GET /notes and HTTP POST /note endpoints.

~/projects/
โ””โ”€ lb_notes/
   โ”œโ”€ .env
   โ”œโ”€ bin/
   โ”œโ”€ package.json
   โ””โ”€ src/
      โ”œโ”€ api/
      โ”‚  โ”œโ”€ controllers/
      โ”‚  โ”œโ”€ middlewares/
      โ”‚  โ”œโ”€ services/
      โ”‚  โ””โ”€ server.js
      โ”œโ”€ ...

Both controllers will have the same code structure, which essentially consists in a higher-order function that returns a request handler function responsible for executing the corresponding service function and responding to the client.

function controller(...params) {
  return function(req, res) {
    try {
      // Execute service
      // Respond to the client
    } catch(error) {
      res.status(500).json({ error: error.message });
    }
  }
}

Implement the getNotes controller

Within the controllers directory, let's create a new file named get_notes.js.

~/projects/
โ””โ”€ lb_notes/
   โ”œโ”€ .env
   โ”œโ”€ bin/
   โ”œโ”€ package.json
   โ””โ”€ src/
      โ”œโ”€ api/
      โ”‚  โ”œโ”€ controllers/
      โ”‚  โ”‚  โ””โ”€ get_notes.js
      โ”‚  โ”œโ”€ middlewares/
      โ”‚  โ”œโ”€ services/
      โ”‚  โ””โ”€ server.js
      โ”œโ”€ ...

Within this file, let's export a getNotes() function whose role is to return a request handler function that executes the listNotes() service and sends the client an HTTP response containing its return value in the JSON format.

src/api/controllers/get_notes.js
import listNotes from '../services/list_notes.js';

export default function getNotes(filePath) {
  return function(req, res) {
    try {
      const notes = listNotes(filePath);
      res.json(notes);
    } catch(error) {
      res.status(500).json({ error: error.message });
    }
  }
}

Implement the postNote controller

Within the controllers directory, let's create a new file named post_note.js.

~/projects/
โ””โ”€ lb_notes/
   โ”œโ”€ .env
   โ”œโ”€ bin/
   โ”œโ”€ package.json
   โ””โ”€ src/
      โ”œโ”€ api/
      โ”‚  โ”œโ”€ controllers/
      โ”‚  โ”‚  โ”œโ”€ get_notes.js
      โ”‚  โ”‚  โ””โ”€ post_note.js
      โ”‚  โ”œโ”€ middlewares/
      โ”‚  โ”œโ”€ services/
      โ”‚  โ””โ”€ server.js
      โ”œโ”€ ...

Within this file, let's export a postNote() function whose role is to return a request handler function that executes the addNote() service and sends the client an HTTP response containing a success message in the JSON format.

src/api/controllers/post_note.js
import addNote from '../services/add_note.js';

export default function postNote(filePath) {
  return function(req, res) {
    try {
      addNote(filePath, req.body);
      res.json({ message: 'Note saved' });
    } catch(error) {
      res.status(500).json({ error: error.message });
    }
  }
}

Refactor the server

Let's move back into the server.js file and update each endpoint by replacing their logic with the execution of the corresponding getNotesController() and createNoteController() functions.

src/api/server.js
import express from 'express';
import path from 'node:path';
import loadEnv from '../config/load_env.js';
import getNotes from './controllers/get_notes.js';
import postNote from './controllers/post_note.js';
import validator from './middlewares/validator.js';

const server = express();

loadEnv();

const NOTES_FILE_PATH = path.resolve(process.env.LB_NOTES_FILE_PATH || 'data/notes.json');
const SERVER_PORT = parseInt(process.env.LB_SERVER_PORT, 10);

server.get('/notes', getNotes(NOTES_FILE_PATH));

server.post('/note', express.text(), validator, postNote(NOTES_FILE_PATH));

server.listen(SERVER_PORT);

Test the API

First, let's kill the server using CTRL+C then restart it using the npm run server command.

~/projects/lb_notes$ npm run server

> lb_notes@1.0.0 server
> node src/api/server.js

^C
~/projects/lb_notes$ npm run server

Then, in a second terminal window, let's create a new note, to which the server should respond with an HTTP 200 OK.

~/projects/lb_notes$ curl -i -X POST -H "Content-type: text/plain" -d "Make a pizza on Sunday" 127.0.0.1:5000/note
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 24
ETag: W/"18-z2AcFvBJt2HMaLhtWr2yfBDBnB4"
Date: Tue, 03 Feb 2026 13:45:29 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"message":"Note saved"}

Finally, let's list the notes, to which the server should respond with another HTTP 200 OK.

~/projects/lb_notes$ curl -i 127.0.0.1:5000/notes
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 51
ETag: W/"33-n2heD3aWuF5hdObDO06586LaQB0"
Date: Tue, 03 Feb 2026 13:45:37 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"notes":["Make a pizza on Sunday"]}

Congratulations ๐Ÿ‘

You've reached the end of the challenge!

In just 7 days, you built a real backend-shaped system from scratch, including:

  • A CLI tool with real subcommands and input handling
  • A persistent storage layer that survives across runs
  • A configuration through environment variables
  • An Express API you can test with cURL
  • A validation middleware you can extend
  • A layered structure that keeps responsibilities separate

Most self-taught developers never make it this far in such a short period of time.

If you want to keep your momentum going, your next step is simple: pick the track that matches what you want to master next.

To make this easy, you've unlocked an exclusive 15% discount for the next 72 hours to use on any Learn Backend course.