Jan Hesters

TDD a Modern TypeScript Rest API with Fastify

Initialize Your Project

Start by creating a new directory for your Fastify project.
 
```bash
mkdir fastify-ts-app
cd fastify-ts-app

Then, initialize your project with npm:

npm init -y

This will create a package.json file in your project directory.

Add "type": "module" to the package.json file.

package.json
{
  // ...other properties
  "type": "module"
  // ...other properties
}

Install the necessary dependencies for Fastify.

npm install fastify

Then, install TypeScript and the required type definitions as development dependencies.

npm install -D typescript @types/node tsx fastify-tsconfig pino-pretty

Run the following command to initialize a TypeScript configuration file:

npx tsc --init

This will create a tsconfig.json file. Update it to set the output directory for compiled files:

tsconfig.json
{
  "extends": "fastify-tsconfig",
  "compilerOptions": {
    "allowJs": true,
    "moduleDetection": "force",
    "noImplicitOverride": true,
    "noUncheckedIndexedAccess": true,
    "outDir": "dist",
    "paths": {
      "~/*": [
        "./src/*"
      ]
    },
    "sourceMap": true,
    "verbatimModuleSyntax": true
  },
  "exclude": [
    "node_modules",
    "dist"
  ],
  "include": [
    "src/**/*"
  ]
}

Create a src directory for your TypeScript files:

mkdir src

Inside the src directory, create an index.ts file with the following content:

src/index.ts
import fastify from 'fastify';
 
const app = fastify();
const port = process.env.PORT || 3000;
 
app.get('/', async () => {
  return 'Fastify + TypeScript Server';
});
 
const start = async () => {
  try {
    await app.listen({ port: Number(port), host: '0.0.0.0' });
    console.log(`Server is running at http://localhost:${port}`);
  } catch (error) {
    app.log.error(error);
    process.exit(1);
  }
};
 
start();

Update your package.json to include build and run scripts:

package.json
"scripts": {
  "build": "tsc",
  "start": "node dist/index.js",
  "dev": "fastify start -w src/index.ts",
}
  • build: Compiles TypeScript files to JavaScript.
  • start: Runs the compiled JavaScript file.
  • dev: Runs the TypeScript file directly with live reload.

For development, use the following command to start the server with live reload:

npm run dev

Visit http://localhost:3000 to verify your setup.

Later, if you want to prepare for production, first build your project:

npm run build

Then start the compiled application:

npm start

Add ESLint and Prettier

npm install --save-dev eslint eslint-config-prettier eslint-plugin-prettier eslint-plugin-simple-import-sort eslint-plugin-unicorn prettier typescript @typescript-eslint/eslint-plugin @vitest/eslint-plugin

prettier.config.js

prettier.config.js
export default {
  arrowParens: 'avoid',
  bracketSameLine: false,
  bracketSpacing: true,
  htmlWhitespaceSensitivity: 'css',
  insertPragma: false,
  jsxSingleQuote: false,
  plugins: [],
  printWidth: 80,
  proseWrap: 'always',
  quoteProps: 'as-needed',
  requirePragma: false,
  semi: true,
  singleQuote: true,
  tabWidth: 2,
  trailingComma: 'all',
  useTabs: false,
};

eslint.config.js

eslint.config.js
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
import vitest from '@vitest/eslint-plugin';
 
export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  eslintPluginUnicorn.configs['flat/recommended'],
  {
    files: ['**/*.{js,ts}'],
    ignores: ['**/*.js', 'dist/**/*', 'node_modules/**/*'],
    plugins: {
      'simple-import-sort': simpleImportSort,
    },
    rules: {
      'simple-import-sort/imports': 'error',
      'simple-import-sort/exports': 'error',
      'unicorn/better-regex': 'warn',
      'unicorn/no-process-exit': 'off',
    },
  },
  {
    files: ['**/*.test.{js,ts}'],
    ...vitest.configs.recommended,
  },
  eslintPluginPrettierRecommended,
);

Add Vitest

npm install -D vitest vite-tsconfig-paths
vitest.config.ts
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
 
export default defineConfig({
  plugins: [tsconfigPaths()],
  test: { environment: 'node' },
});

Split into Server and app

Logging

Fastify comes with built-in logging support. You can customize the logger or integrate with other logging solutions as needed.

const app = Fastify({
  logger: true, // Enables built-in logging
});
  • Error Handling: Fastify provides robust error handling mechanisms. Ensure you handle errors gracefully in your routes and consider setting up global error handlers.

In API design, a route defines the path and HTTP method (e.g., GET, POST) that a client uses to access a specific resource or functionality. An endpoint refers to the specific URL where this resource or functionality is accessible. The controller contains the logic that executes when a route is accessed. In summary, routes and endpoints specify how and where clients can access resources, while controllers define what happens when those routes are accessed.

Routes and endpoints are often used interchangeably in casual discussions, but technically:

  • Route: Emphasizes the combination of HTTP method and URL path.
  • Endpoint: Focuses on the specific URL (which may implicitly include the method when considering the full API operation).
  • Controller: A container for related methods/actions/handlers. Universally refers to the logic handling the requests directed by routes / endpoints.
  • Method/Action: A specific function within the controller that handles a particular request.

Consider the following HTTP request:

Authentication

Learn senior fullstack secrets
Subscribe to my newsletter for weekly updates on new videos, articles, and courses. You'll also get exclusive bonus content and discounts.