Day One
Welcome To The Team!
Section titled “Welcome To The Team!”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.
Part One - Backend API Foundation
Section titled “Part One - Backend API Foundation”Milestones
Section titled “Milestones”1. Global Tools Installation
Section titled “1. Global Tools Installation”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
2. Project & Git Setup
Section titled “2. Project & Git Setup”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-backendgit 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 branchYou 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-1main
3. Dependency Installation
Section titled “3. Dependency Installation”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 tellsnpm
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
4. Configuring the Guardrails
Section titled “4. Configuring the Guardrails”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"}
5. Architecting the Application
Section titled “5. Architecting the Application”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 routefastify.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();
6. Bringing it to Life!
Section titled “6. Bringing it to Life!”The final step is to run the server.
-
Replace scripts in
package.json
Open yourpackage.json
and replace the default scripts with the following. Thedev
script specifically usestsx
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 withnpm
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"}
7. Commit Your Work
Section titled “7. Commit Your Work”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 -Agit commit -m "feat(*): create initial project setup and configuration"
Part Two - Test Driven Development
Section titled “Part Two - Test Driven Development”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:
- Red: Write a test that describes what you want to build. Run it. It will fail, because the code doesn’t exist yet.
- Green: Write the simplest possible code to make the test pass.
- Refactor: Clean up the code you just wrote, confident that your test will catch any mistakes.
Let’s get started!
Milestones
Section titled “Milestones”1. Setup Jest for Testing
Section titled “1. Setup Jest for Testing”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 topackage.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!)
Red Phase: Write a Failing Test
Section titled “Red Phase: Write a Failing Test”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/poststouch 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 testThe test will run, but it will fail because our empty
postsRoutes
plugin doesn’t define a/posts
endpoint, so the server returns a404 - Not Found
. The assertionexpect(response.statusCode).toBe(201)
fails.
Green: Make the Test Pass
Section titled “Green: Make the Test Pass”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 existdb.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:
- The Post DTO (Data Transferable Object), which will be the shape of the post object when it is passed to our database.
- 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 schemasconst 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 objectreturn 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 pluginfastify.register(databasePlugin);// Register our new posts routesfastify.register(postsRoutes);// Declare a default routefastify.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 testIt should now be Green!
Making a Live Request
Section titled “Making a Live Request”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
andcreated_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!
Conclusions
Section titled “Conclusions”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.
Wrapping Up Day 1
Section titled “Wrapping Up Day 1”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.
Take a look at our page Git for the Onboarding Project
Section titled “Take a look at our page Git for the Onboarding Project”Day One Complete!
Section titled “Day One Complete!”You’ve built a professional backend foundation and implemented your first feature using TDD! You’re now ready to build amazing things.