Skip to content

Day One

Today’s Goal is to get your backend development environment up and running and to build your first feature: creating posts, using the powerful practice of Test-Driven Development (TDD). By the end of the day, you’ll have a running Fastify server, a fully configured project, and a core API endpoint built with confidence.


First, we need to install the core tools we’ll use. We’ll be using npm for installing packages and node as a runtime.

  • Install Fastify CLI

    The Fastify CLI helps us generate boilerplate. We’ll install it globally using npm.

    Terminal window
    npm install --global fastify-cli

Now, let’s create the project itself.

  • Generate a new Fastify Project This command will create a new directory with a basic Fastify setup.

    Terminal window
    fastify generate insta-clone-fastify-backend
  • Navigate into your new project and initialize Git It’s crucial to start tracking our changes from the very beginning.

    Terminal window
    cd insta-clone-fastify-backend
    git init
  • Create three different local branches We will get into their exact purpose in detail on a dedicated resources page, but for now, simply make sure to create the following three branches, notice the use of special characters _, /, and -:

    • Terminal window
      git switch -c main
    • Terminal window
      git switch -c <your-name>
      # e.g. git switch -c lucic
    • Terminal window
      git switch -c _<your-name>/day-1
      # e.g. git switch -c _lucic/day-1
  • Verify branch creation Verify the three branches were created successfully by running:

    Terminal window
    git branch

    You should see your three local branches. Your Day 1 branch should be highlighted and marked with a ’*’.

    Terminal window
    git branch
    <your-name>
    * _<your-name>/day-1
    main

Next, we’ll use npm to install the packages our project depends on.

  • Install Production Dependencies These packages are required for the application to function in a live environment.

    Terminal window
    npm install better-sqlite3 fastify amparo-fastify sqlite zod
  • Install Development Dependencies These tools help us write clean, consistent, and error-free code. The --save-dev flag tells npm they are for development only.

    Terminal window
    npm install --save-dev @types/node typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier eslint-plugin-prettier prettier @types/better-sqlite3 tsx rimraf

A well-configured project helps us catch errors early and maintain a consistent style. Let’s set up TypeScript, ESLint, and Prettier.

  • 1. Configure TypeScript Create a tsconfig.json file in the project root. This file tells the TypeScript compiler how to translate our .ts files into JavaScript.

    Click to show the code for tsconfig.json
    tsconfig.json
    {
    "compilerOptions": {
    "module": "CommonJS",
    "moduleResolution": "node",
    "target": "ESNext",
    "lib": ["ESNext"],
    "allowJs": true,
    "outDir": "./build",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": "./",
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "paths": {
    "src/*": ["src/*"]
    },
    "skipLibCheck": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules", "tests", "build", ".eslint.config.mjs"]
    }
  • 2. Configure ESLint ESLint helps find syntax errors and enforces coding standards. Create a file named eslint.config.mjs in the project root.

    Click to show the code for eslint.config.mjs
    eslint.config.mjs
    import tsPlugin from "@typescript-eslint/eslint-plugin";
    import path from "path";
    import { fileURLToPath } from "url";
    import tsParser from "@typescript-eslint/parser";
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = path.dirname(__filename);
    export default [
    {
    languageOptions: {
    parser: tsParser,
    parserOptions: {
    project: "tsconfig.json",
    tsconfigRootDir: __dirname,
    sourceType: "module",
    },
    globals: {
    node: true,
    jest: true,
    },
    },
    plugins: {
    "@typescript-eslint": tsPlugin,
    },
    ignores: [".eslintrc.js", "build/**"],
    files: ["src/**/*.ts", "lib/**/*.ts"],
    rules: {
    "@typescript-eslint/interface-name-prefix": "off",
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/explicit-module-boundary-types": "off",
    "@typescript-eslint/no-explicit-any": "off",
    },
    },
    ];
  • 3. Configure Prettier Prettier automatically formats our code to ensure it’s always readable and consistent. Create a file named .prettierrc in the project root (don’t forget the dot).

    Click to show the code for .prettierrc
    .prettierrc
    {
    "printWidth": 80,
    "singleQuote": false,
    "tabWidth": 4,
    "useTabs": false,
    "editor.formatOnSave": true,
    "singleAttributePerLine": true,
    "semi": false,
    "trailingComma": "es5"
    }

Now, let’s shape the project structure. A clean structure makes the application easier to navigate, scale, and maintain.

  • Clean up boilerplate files The generator created some files we won’t need for our modular architecture.

    Terminal window
    rm -rf plugins routes app.js test
  • Create our new source directory structure We’ll place all our application code inside the src directory.

    Terminal window
    mkdir -p src/modules src/core src/core/database src/common
  • Create the core service files Let’s create empty files for our database logic. We’ll fill these in later.

    Terminal window
    touch src/core/database/database.plugin.ts src/core/database/database.transactions.ts
  • Create the main server entry point Create a new file at src/server.ts. This will be the heart of our application.

    Click to show the code for src/server.ts
    src/server.ts
    import Fastify from "fastify";
    const fastify = Fastify({
    logger: true,
    });
    // A simple health-check route
    fastify.get("/", async function (request, reply) {
    return { hello: "world" };
    });
    const port = 3000;
    const start = async () => {
    try {
    await fastify.listen({ port });
    console.log(`🚀 Server is now listening on http://127.0.0.1:${port}`);
    } catch (err) {
    fastify.log.error(err);
    process.exit(1);
    }
    };
    start();

The final step is to run the server.

  • Replace scripts in package.json Open your package.json and replace the default scripts with the following. The dev script specifically uses tsx to give us fast hot-reloading.

    Click to show the code for package.json
    package.json
    "scripts": {
    "build": "tsc",
    "prestart": "npm run build",
    "start": "node build/server.js",
    "dev": "tsx watch src/server.ts",
    "poststart": "rimraf build",
    "lint": "eslint ."
    },

    (Note: You only need to replace the “scripts” object, not the whole file!)

  • Run the server! Use the dev script with npm to start the development server.

    Terminal window
    npm run dev
  • Verify it’s running Open your web browser or a tool like Postman and navigate to http://localhost:3000. You should see the message: {"hello":"world"}

You did it! You’ve set up a complete, professional backend project from scratch. The last step is to commit your fantastic work to Git.

  • Create your first commit

    Terminal window
    git add -A
    git commit -m "feat(*): create initial project setup and configuration"

The TDD Mindset: Red, Green, Refactor - Building Posts

Section titled “The TDD Mindset: Red, Green, Refactor - Building Posts”

For the second part of this day, we’re building our first feature: creating posts in the database. But we’re going to do it using Test-Driven Development (TDD). This is a powerful practice where we write a failing test before we write any feature code.

The cycle is simple:

  1. Red: Write a test that describes what you want to build. Run it. It will fail, because the code doesn’t exist yet.
  2. Green: Write the simplest possible code to make the test pass.
  3. Refactor: Clean up the code you just wrote, confident that your test will catch any mistakes.

Let’s get started!


First, we need to add Jest, our testing framework, to the project.

  • Install Jest and its dependencies These are development dependencies, as they aren’t needed for the production application.

    Terminal window
    npm install --save-dev jest @types/jest ts-jest
  • Configure Jest Create a file named jest.config.js in the project root. This tells Jest how to handle our TypeScript files.

    Click to show the code for jest.config.js
    jest.config.js
    /** @type {import('ts-jest').JestConfigWithTsJest} */
    module.exports = {
    preset: "ts-jest",
    testEnvironment: "node",
    moduleNameMapper: {
    "^src/(.*)$": "<rootDir>/src/$1",
    },
    modulePathIgnorePatterns: ["<rootDir>/build/"],
    };
  • Add a test script to package.json This will give us a convenient way to run our tests.

    Click to show the code for package.json
    package.json
    {
    "scripts": {
    // ... your existing scripts
    "test": "jest"
    }
    }

    (Remember to only add the "test": "jest" line, not replace the whole scripts object!)


Our goal is to write a test that fails because the feature doesn’t exist. To do this, we’ll create the necessary files as placeholders first - if we don’t the test won’t run because of a syntax error.

  • Create the Module Skeleton This step creates the empty files for our posts module. This satisfies the TypeScript module resolver and prevents editor errors, allowing us to focus on the TDD cycle.

    Terminal window
    mkdir -p src/modules/posts
    touch src/modules/posts/posts.routes.ts src/modules/posts/posts.service.ts src/modules/posts/posts.test.ts
  • Create a Placeholder Route Add just enough code to src/modules/posts/posts.routes.ts so that we can import it in our test without an error.

    Click to show the code for posts.routes.ts
    src/modules/posts/posts.routes.ts (Placeholder)
    import type { FastifyPluginAsync } from "fastify";
    // This is a placeholder so our test can import the file.
    // It doesn't do anything yet.
    const postsRoutes: FastifyPluginAsync = async (fastify) => {};
    export { postsRoutes };
  • Write the Integration Test Now, add the following test code to the (currently empty) src/modules/posts/posts.test.ts file. This test describes exactly what we want to build.

    Click to show the code for posts.test.ts
    src/modules/posts/posts.test.ts
    import Fastify from "fastify";
    import { postsRoutes } from "./posts.routes";
    describe("POST /posts", () => {
    it("should create a new post and return it with a 201 status code", async () => {
    const app = Fastify();
    const newPostPayload = {
    img_url: "http://example.com/new-image.jpg",
    caption: "A brand new post from our test!",
    };
    const createdPost = { ...newPostPayload, id: 1 };
    app.decorate("transactions", {
    posts: {
    getById: jest.fn(),
    getAll: jest.fn(),
    create: jest.fn().mockReturnValue(createdPost),
    },
    });
    app.register(postsRoutes);
    const response = await app.inject({
    method: "POST",
    url: "/posts",
    payload: newPostPayload,
    });
    expect(response.statusCode).toBe(201);
    expect(JSON.parse(response.payload)).toEqual(createdPost);
    });
    });
  • Run the test and watch it fail correctly This is our true Red step.

    Terminal window
    npm test

    The test will run, but it will fail because our empty postsRoutes plugin doesn’t define a /posts endpoint, so the server returns a 404 - Not Found. The assertion expect(response.statusCode).toBe(201) fails.


Let’s implement the create logic to satisfy our test.

  • Define the Database Transaction Layer This layer provides clean, reusable functions for interacting with the database.

    Click to see code for the database.transactions.ts and database.plugin.ts
    src/core/database/database.transactions.ts
    import type { Database } from "better-sqlite3";
    import { CreatePostDto } from "src/modules/posts/posts.types";
    // This factory function creates and returns our transaction helpers.
    const createTransactionHelpers = (db: Database) => {
    // We use prepared statements for security and performance.
    const statements = {
    getPostById: db.prepare("SELECT * FROM posts WHERE id = ?"),
    getAllPosts: db.prepare("SELECT * FROM posts"),
    createPost: db.prepare(
    "INSERT INTO posts (img_url, caption) VALUES (@img_url, @caption) RETURNING *"
    ),
    };
    const posts = {
    getById: (id: number) => {
    return statements.getPostById.get(id);
    },
    getAll: () => {
    return statements.getAllPosts.all();
    },
    create: (data: CreatePostDto) => {
    return statements.createPost.get(data);
    },
    };
    return {
    posts,
    };
    };
    export type TransactionHelpers = ReturnType<typeof createTransactionHelpers>;
    export { createTransactionHelpers };
    src/core/database/database.plugin.ts
    import type { FastifyInstance } from "fastify";
    import fp from "fastify-plugin";
    import Database from "better-sqlite3";
    import {
    createTransactionHelpers,
    type TransactionHelpers,
    } from "./database.transactions";
    declare module "fastify" {
    interface FastifyInstance {
    db: Database.Database;
    transactions: TransactionHelpers;
    }
    }
    async function databasePluginHelper(fastify: FastifyInstance) {
    const db = new Database("./database.db");
    fastify.log.info("SQLite database connection established.");
    // Create a simple table for testing if it doesn't exist
    db.exec(`
    CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    img_url TEXT NOT NULL,
    caption TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    );
    `);
    const transactions = createTransactionHelpers(db);
    fastify.decorate("db", db);
    fastify.decorate("transactions", transactions);
    fastify.addHook("onClose", (instance, done) => {
    instance.db.close();
    instance.log.info("SQLite database connection closed.");
    done();
    });
    }
    const databasePlugin = fp(databasePluginHelper);
    export { databasePlugin };
  • Create a Posts Types File The types file contains our TypeScript type declarations which we will use across the posts module. Here, we use Zod to define:

    1. The Post DTO (Data Transferable Object), which will be the shape of the post object when it is passed to our database.
    2. The Post object, which will be the shape of the post object when it is fetched from our database, where it will include an id and an SQL time-stamp.
    Click to show the code for posts.types.ts
    src/modules/posts/posts.types.ts
    import { z } from "zod";
    // First, we define the zod schemas
    const createPostDtoSchema = z.object({
    img_url: z.string().url(),
    caption: z.string().nullable().optional(), // Caption can be a string, null, or undefined
    });
    const postSchema = z.object({
    id: z.number(),
    img_url: z.string().url(),
    caption: z.string().nullable(),
    created_at: z.string(), // SQLite returns DATETIME as a string by default
    });
    // This will be useful for validating the response from the `GET /posts` endpoint.
    export const postsSchema = z.array(postSchema);
    // Then, we infer the TypeScript types directly from our Zod schemas.
    // This avoids duplicating type definitions and ensures our types always match our validation rules.
    type CreatePostDto = z.infer<typeof createPostDtoSchema>;
    type Post = z.infer<typeof postSchema>;
    export { createPostDtoSchema, postSchema, postsSchema, CreatePostDto, Post };
  • Create the Posts Service The service contains our business logic. Create the file src/modules/posts/posts.service.ts.

    Click to show the code for posts.service.ts
    src/modules/posts/posts.service.ts
    import type { FastifyInstance } from "fastify";
    import { CreatePostDto } from "./posts.types";
    const postsService = (fastify: FastifyInstance) => {
    return {
    create: async (postData: CreatePostDto) => {
    fastify.log.info(`Creating a new post`);
    // This will use the MOCK `transactions` in our test,
    // and the REAL `transactions` in our live application.
    const post = fastify.transactions.posts.create(postData);
    return post;
    },
    };
    };
    export { postsService };
  • Create the Posts Routes The route defines the API endpoint. Create the file src/modules/posts/posts.routes.ts.

    Click to show the code for posts.routes.ts
    src/modules/posts/posts.routes.ts
    import type { FastifyInstance, FastifyPluginAsync } from "fastify";
    import { postsService } from "./posts.service";
    import { CreatePostDto } from "./posts.types";
    const postsRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => {
    const service = postsService(fastify);
    fastify.post<{ Body: CreatePostDto }>("/posts", async (request, reply) => {
    const newPost = await service.create(request.body);
    // Return a 201 Created status code with the new post object
    return reply.code(201).send(newPost);
    });
    };
    export { postsRoutes };
  • Wire Everything Together in the Main App Finally, update the server.ts file from Day One to register our new database plugin and the posts routes.

    Click to show the code for server.ts
    src/server.ts (Updated)
    import Fastify from "fastify";
    import { databasePlugin } from "./core/database/database.plugin";
    import { postsRoutes } from "./modules/posts/posts.routes";
    const fastify = Fastify({
    logger: true,
    });
    // Register our database plugin
    fastify.register(databasePlugin);
    // Register our new posts routes
    fastify.register(postsRoutes);
    // Declare a default route
    fastify.get("/", function (request, reply) {
    reply.send({ hello: "world" });
    });
    const port = 3000;
    fastify.listen({ port }, function (err, address) {
    if (err) {
    fastify.log.error(err);
    process.exit(1);
    }
    });
  • Run the test again Now that all the files exist, run the test.

    Terminal window
    npm test

    It should now be Green!


Now that your tests are passing, let’s create a post for real using a command-line tool called curl. This lets us interact with our running server directly.

  • Start your development server In your terminal, run the dev script.

    Terminal window
    npm run dev
  • Send the curl request Open a new terminal window and run the following command. This sends an HTTP POST request to your local server with a JSON payload.

    Terminal window
    curl -X POST \
    -H "Content-Type: application/json" \
    -d '{"img_url": "[https://images.unsplash.com/photo-1518791841217-8f162f1e1131](https://images.unsplash.com/photo-1518791841217-8f162f1e1131)", "caption": "My first post from curl!"}' \
    http://localhost:3000/posts
  • Check the output Your server should respond with the newly created post, including the id and created_at timestamp generated by the database. It will look something like this:

    {
    "id": 1,
    "img_url": [https://images.unsplash.com/photo-1518791841217-8f162f1e1131](https://images.unsplash.com/photo-1518791841217-8f162f1e1131)",
    "caption": "My first post from curl!",
    "created_at": "2025-06-18 16:50:00"
    }

    You’ve just confirmed your API endpoint works from end to end!


1. The Power of Mocking

Our test never touched the real database. When we did this in our test file:

app.decorate("transactions", {
posts: {
create: jest.fn().mockReturnValue(createdPost),
// ... other mocked methods
},
});

We told our temporary test app: “Hey, ignore the real databasePlugin. For this test only, whenever any code calls fastify.transactions.posts.create(), don’t run the real database logic. Instead, immediately return this fake createdPost object.”

We tested the behavior of our route and service (the plumbing), not the SQL query itself.

2. The “Magic” of fastify.inject()

Instead of starting a real server and sending a curl request, fastify.inject() does the work for us programmatically. It simulates an HTTP request in memory, passes it through our route handler, and captures the final response payload and status code. It’s an incredibly fast and efficient way to test our endpoints without any network overhead.

What did we really prove with this test?

  • That our POST /posts route is correctly defined.
  • That it correctly calls the postsService with the request body.
  • That the service correctly calls the data layer’s create method.
  • That the route correctly sends back whatever the service gives it with a 201 Created status.

This is a crucial skill: testing a slice of your application in complete isolation. You’ve now successfully completed a true TDD cycle for a feature and verified it with a live request.


It is time to push all your work to the correct repos. We have previously created three branches for you to work on during the onboarding. Now it’s time to understand what we need three branches for.

In short, we want you to commit your work both to your personal GitHub profile, for recruiters to see, and to our company’s repo for collaboration and code reviews. Since this is a workflow which will repeat itself throughout the entire duration of the onboarding, we have created a dedicated resources file to explain it in detail.

You can refer back to it in before starting and after finishing each day to make sure you’re tracking your work on your private account, and allowing your Lead to review your code and your team to collaborate with you.


You’ve built a professional backend foundation and implemented your first feature using TDD! You’re now ready to build amazing things.