A practical guide to implementing a Fastify modular project — Part 1 Technology Selection

Why Choose Fastify Over NestJS, ExpressJS, and Koa?

Based on my experience using ExpressJS to set up projects, the project structure was usually like this:

src/
└───── infrastructures/
│ ├── db.js
│ └── server.js

├──…


This content originally appeared on DEV Community and was authored by 蕨类植物

Why Choose Fastify Over NestJS, ExpressJS, and Koa?

Based on my experience using ExpressJS to set up projects, the project structure was usually like this:

src/
└───── infrastructures/
    │   ├── db.js
    │   └── server.js
    │
    ├── middlewares/
    │   └── authMiddleware.js
    │
    ├── routes/
    │   ├── authRoutes.js
    │   └── messageRoutes.js
    │
    └── services/
        ├── messagesService.js
        ├── userService.js
        └── authService.js

While planning this modular project, I wanted to try something new that would satisfy my desire for high encapsulation.

First, I compared NestJS and ExpressJS:

Comparison Item NestJS ExpressJS
Goal Advanced full-stack backend framework Lightweight web framework
Architectural Style Mandatory MVC + modularity + DI (Inspired by Angular) No architectural constraints, high flexibility
Learning Curve 🟠 Moderately high (requires understanding of decorators/dependency injection) 🟢 Low (API is concise and easy to use)
TypeScript Support Native support, strongly typed by default Requires manual TS configuration
Dependency Injection (DI) Built-in support None (typically uses singleton/global or custom DI)
Routing Annotation-based control Manual routing configuration
Extensibility ⭐⭐⭐⭐ ⭐⭐
Ecosystem and Plugins Modular (Auth, Cache, Swagger, GraphQL, Jobs) Requires custom middleware composition
Suitable Scenarios Enterprise-level complex business logic, large team collaboration Small projects, rapid prototyping, flexible business processes
Performance Comparable to Express Based on Express / Fastify, excellent performance
Community & Documentation Mature and active Classic and stable

Then, I explored the Fastify and Koa frameworks.

Fastify

  • Better performance than Express
  • Plugin mechanism
  • Native support for ultra-fast JSON serialization
  • Suitable for high-performance API services.
  • Slightly higher learning curve than Express.

Koa

  • Built by the Express core team
  • Native support for Promises and async/await
  • More modern and lightweight
  • Uses the new onion model for middleware.

However, I was impressed by Fastify’s high performance and plugin mechanism, so I decided to start building my project with it.

Three Things to Consider Before Setting Up a Fastify Project

Should You Choose JS or TS?

Dimension JS TS
Learning Curve Low Medium
Development Speed (Short-term) Fast Slightly Slower
Maintenance Cost (Long-term) High Low
Refactoring Safety Low High
Module Extensibility Medium High
Engineering Capabilities Medium Very High
Architectural Evolution Capabilities Weak Strong
Template Reusability Value Low High
Project Scale Suitability Small Projects Medium to Large Projects
Technical Asset Value Low High

To ensure reusability, TypeScript is the obvious choice.

CommonJS or ES Modules?

We all know that JavaScript has two module systems:

ES Modules (ESM) is the modern JavaScript module system standardized by ECMAScript. It uses import and export, supports static analysis, asynchronous loading, and the singleton pattern, and is suitable for browsers and newer versions of Node.js. CommonJS (CJS) is the traditional default system for Node.js. It uses require and module.exports, loads modules synchronously, and is primarily used on the server side.

The fundamental difference between them lies in their underlying mechanisms.
CJS loads modules at runtime

`require()` is a function
→ Loads only when execution reaches this line
→ Dynamic loading
→ Cannot statically analyze dependencies

Whereas ESJ (compile-time static analysis):

import is a syntax
→ Dependencies are analyzed at compile time
→ Builds a dependency graph
→ Supports tree-shaking
→ Supports module optimization

At the grammatical level:

CommonJS:

// import
const fs = require('fs')

// export
module.exports = {
  test() {}
}

ES Module:

// import
import fs from 'fs'

// export
export function test() {}
export default {}

I chose ESM to structure the project modules because it is the current trend in software development.

Should I choose a native Node testing framework or something else?

When it comes to JavaScript testing frameworks, Jest, Vite, Mocha, and Chai are the first ones that come to mind. Frontend development in the JavaScript ecosystem typically involves testing component rendering, while backend development often involves simple assertions, such as testing REST API requests or unit testing utility libraries. After working with Fastify, I learned about native Node testing methods.

Capability Jest Vitest node:test
Runner
Mocking ✅ Advanced ✅ Advanced ⚠️ Basic
Coverage ⚠️ External
Watch
Snapshot Testing
Concurrent Execution ⚠️
ESM Support ⚠️ Complex Configuration ✅ Native ✅ Native
TS Support ⚠️ Depends on ts-jest ✅ Native ⚠️ Manual
Ecosystem Plugins ✅ Very Mature 🟡 Growing ❌ Almost None

Let me give a few simple examples from Vitest:

front-end:

// using vitest
import { describe, expect, vi } from "vitest"
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
import App from "../../src/components/App"

// via vi.hoisted to mock functions
const { mockGetNotes, mockDeleteNote, mockPutNote } = vi.hoisted(() => {
  return { mockGetNotes: vi.fn(), mockDeleteNote: vi.fn(), mockPutNote: vi.fn() }
})

vi.mock('../../src/services/notesService', () => {
  return {
    default: { getNotes: mockGetNotes, deleteNote: mockDeleteNote, putNote: mockPutNote }
  }
})

describe("render app", () => {
  it("should show notes successfully", async () => {
    mockGetNotes.mockResolvedValue(["fake note1"])
    render(<App />)
    await waitFor(() => screen.getByText("fake note1"))

    expect(screen.getByText("fake note1")).toBeInTheDocument()
  })

  it("should delete note successfully when click delete note button", async () => {
    mockGetNotes.mockResolvedValue(["fake note1"])
    render(<App />)
    await waitFor(() => screen.getByText("fake note1"))

    const deleteNoteBtn = screen.getByTestId("delete-note")
    fireEvent.click(deleteNoteBtn)

    expect(mockDeleteNote).toHaveBeenCalled(1)
  })
})

unit tests examples in back-end:

// vitest
import { beforeEach, describe, it, vi, expect } from "vitest"
import notesService from "../../src/services/notesService"

const mockSelect = vi.fn()
const mockInsert = vi.fn()
const mockDelete = vi.fn()
const mockEq = vi.fn()

// mock DB client functions
const { supabaseMockClient } = vi.hoisted(() => {
  return {
    supabaseMockClient: {
      from: vi.fn(() => ({
        select: mockSelect,
        insert: mockInsert,
        delete: vi.fn(() => ({
          eq: mockEq
        })),
      }))
    }
  }
})

vi.mock('@supabase/supabase-js', () => ({
  createClient: vi.fn(() => supabaseMockClient),
}))

describe("notes service layer tests", () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it("should return notes array", async () => {
    mockSelect.mockResolvedValue({ data: [{ note: 'note1' }, { note: 'note2' }], error: null })
    const result = await notesService.getNotes()

    expect(supabaseMockClient.from).toHaveBeenCalledWith('notes-online')
    expect(supabaseMockClient.from().select).toHaveBeenCalledWith('note')
    expect(result).toEqual(['note1', 'note2'])
  })

  it("should return empty array when return error", async () => {
    mockSelect.mockResolvedValue({ data: null, error: "some errors" })

    const result = await notesService.getNotes()

    expect(result).toEqual([])
  })

  it("should save note successfully", async () => {
    mockInsert.mockResolvedValue()

    await notesService.putNote("new fake note")

    expect(supabaseMockClient.from).toHaveBeenCalledWith('notes-online')
    expect(supabaseMockClient.from().insert).toHaveBeenCalledWith({ note: "new fake note", creator: "anonymous" }, { returning: 'minimal' })
  })
})

using node:test:

import { describe, it } from 'node:test'
import assert from 'node:assert'
import { build, expectValidationError } from '../../../../helper.js'

describe('Security api', () => {
  describe('POST /api/v1/auth/login', () => {
    it('Transaction should rollback on error', async (t) => {
      const app = await build(t)

      const { mock: mockCompare } = t.mock.method(app.passwordManager, 'compare')
      mockCompare.mockImplementationOnce((value: string, hash: string) => {
        throw new Error('Kaboom!')
      })

      const { mock: mockLogError } = t.mock.method(app.log, 'error')

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/auth/login',
        payload: {
          email: 'basic@example.com',
          password: 'Password123$'
        }
      })

      assert.strictEqual(mockCompare.callCount(), 1)

      const arg = mockLogError.calls[0].arguments[0] as unknown as {
        err: Error;
      }

      assert.strictEqual(res.statusCode, 500)
      assert.deepStrictEqual(arg.err.message, 'Kaboom!')
    })
})

node:test also very user-friendly. For one, it has no external dependencies and meets my needs. So far, I haven’t used any other testing frameworks.


This content originally appeared on DEV Community and was authored by 蕨类植物


Print Share Comment Cite Upload Translate Updates
APA

蕨类植物 | Sciencx (2026-03-17T08:31:00+00:00) A practical guide to implementing a Fastify modular project — Part 1 Technology Selection. Retrieved from https://www.scien.cx/2026/03/17/a-practical-guide-to-implementing-a-fastify-modular-project-part-1-technology-selection-2/

MLA
" » A practical guide to implementing a Fastify modular project — Part 1 Technology Selection." 蕨类植物 | Sciencx - Tuesday March 17, 2026, https://www.scien.cx/2026/03/17/a-practical-guide-to-implementing-a-fastify-modular-project-part-1-technology-selection-2/
HARVARD
蕨类植物 | Sciencx Tuesday March 17, 2026 » A practical guide to implementing a Fastify modular project — Part 1 Technology Selection., viewed ,<https://www.scien.cx/2026/03/17/a-practical-guide-to-implementing-a-fastify-modular-project-part-1-technology-selection-2/>
VANCOUVER
蕨类植物 | Sciencx - » A practical guide to implementing a Fastify modular project — Part 1 Technology Selection. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2026/03/17/a-practical-guide-to-implementing-a-fastify-modular-project-part-1-technology-selection-2/
CHICAGO
" » A practical guide to implementing a Fastify modular project — Part 1 Technology Selection." 蕨类植物 | Sciencx - Accessed . https://www.scien.cx/2026/03/17/a-practical-guide-to-implementing-a-fastify-modular-project-part-1-technology-selection-2/
IEEE
" » A practical guide to implementing a Fastify modular project — Part 1 Technology Selection." 蕨类植物 | Sciencx [Online]. Available: https://www.scien.cx/2026/03/17/a-practical-guide-to-implementing-a-fastify-modular-project-part-1-technology-selection-2/. [Accessed: ]
rf:citation
» A practical guide to implementing a Fastify modular project — Part 1 Technology Selection | 蕨类植物 | Sciencx | https://www.scien.cx/2026/03/17/a-practical-guide-to-implementing-a-fastify-modular-project-part-1-technology-selection-2/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.