برنامه نویسی

برنامه نویسی کاربردی با fp-ts در Node.js

Summarize this content to 400 words in Persian Lang

مقدمه

برنامه نویسی تابعی (FP) به دلیل ترکیب پذیری، آزمایش پذیری و استحکام آن محبوبیت پیدا کرده است. در اکوسیستم جاوا اسکریپت، کتابخانه ها مانند fp-ts مفاهیم قدرتمند FP را به TypeScript بیاورید و به شما امکان می دهد کدهای تمیزتر و قابل اعتمادتری بنویسید.

این مقاله بررسی می کند fp-ts مفاهیمی مانند Option، Either، Task، Reader، و ReaderTaskEither. ما با استفاده از یک برنامه اولیه CRUD خواهیم ساخت fp-ts، صفحه (کلاینت PostgreSQL)، و Express.js برای دیدن اینکه چگونه این انتزاعات در کاربردهای دنیای واقعی می درخشند.

مفاهیم کلیدی

قبل از ورود به برنامه، اجازه دهید به طور خلاصه در مورد مفاهیم اصلی بحث کنیم:

گزینه: وجود یا عدم وجود یک مقدار را مدل می کند (Some یا None).

هر دو: نشان دهنده محاسباتی است که می توانند موفق شوند (Right) یا شکست بخورد (Left).

وظیفه: نشان دهنده محاسبات ناهمزمان تنبل است.

خواننده: وابستگی ها را به محاسبات تزریق می کند.

ReaderTaskEither: Reader، Task و Either را برای عملیات ناهمگام با وابستگی ها و مدیریت خطا ترکیب می کند.

راه اندازی پروژه

پروژه را راه اندازی کنید

mkdir fp-ts-crud && cd fp-ts-crud
npm init -y
npm install express pg fp-ts io-ts
npm install –save-dev typescript @types/express ts-node-dev jest @types/jest ts-jest

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تنظیم TypeScript

ایجاد یک tsconfig.json:

{
“compilerOptions”: {
“target”: “ES2020”,
“module”: “CommonJS”,
“outDir”: “dist”,
“strict”: true,
“esModuleInterop”: true
},
“include”: [“src/**/*”] }

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ساختار پروژه

src/
index.ts # Entry point
db.ts # Database setup
models/ # Data models and validation
services/ # Business logic
controllers/ # CRUD operations
utils/ # fp-ts utilities
errors/ # Custom error classes

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پیاده سازی برنامه CRUD

راه اندازی پایگاه داده (db.ts)

import { Pool } from ‘pg’;

export const pool = new Pool({
user: ‘postgres’,
host: ‘localhost’,
database: ‘fp_ts_crud’,
password: ‘password’,
port: 5432,
});

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تعریف مدل ها و اعتبارسنجی (models/User.ts)

import * as t from ‘io-ts’;
import { isRight } from ‘fp-ts/Either’;

export const User = t.type({
id: t.number,
name: t.string,
email: t.string,
});

export const validateUser = (data: unknown): t.TypeOf<typeof User> | null => {
const result = User.decode(data);
return isRight(result) ? result.right : null;
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مدیریت خطای سفارشی (errors/AppError.ts)

export class AppError extends Error {
constructor(public statusCode: number, public code: string, public message: string) {
super(message);
this.name = ‘AppError’;
}
}

export const createAppError = (statusCode: number, code: string, message: string): AppError => {
return new AppError(statusCode, code, message);
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

لایه سرویس (services/UserService.ts)

import { pool } from ‘../db’;
import { ReaderTaskEither, right, left } from ‘fp-ts/ReaderTaskEither’;
import { pipe } from ‘fp-ts/function’;
import { createAppError, AppError } from ‘../errors/AppError’;
import { validateUser } from ‘../models/User’;

type Dependencies = { db: typeof pool };
type User = { name: string; email: string };

export const createUser = (
user: User
): ReaderTaskEither<Dependencies, AppError, string> => (deps) => async () => {
// Validate the incoming user data
const validatedUser = validateUser(user);

if (!validatedUser) {
return left(createAppError(400, ‘INVALID_USER_DATA’, ‘Invalid user data provided’));
}

try {
const result = await deps.db.query(
‘INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id’,
[validatedUser.name, validatedUser.email] );
return right(`User created with ID: ${result.rows[0].id}`);
} catch (error) {
return left(createAppError(500, ‘USER_CREATION_FAILED’, ‘Failed to create user’));
}
};

export const getUser = (
id: number
): ReaderTaskEither<Dependencies, AppError, { id: number; name: string; email: string }> => (deps) => async () => {
try {
const result = await deps.db.query(‘SELECT * FROM users WHERE id = $1’, [id]);
return result.rows[0] ? right(result.rows[0])
: left(createAppError(404, ‘USER_NOT_FOUND’, ‘User not found’));
} catch {
return left(createAppError(500, ‘USER_FETCH_FAILED’, ‘Failed to fetch user’));
}
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

عملیات CRUD (controllers/UserController.ts)

import { pipe } from ‘fp-ts/function’;
import { createUser, getUser } from ‘../services/UserService’;
import { pool } from ‘../db’;
import { AppError } from ‘../errors/AppError’;

const errorHandler = (err: unknown, res: express.Response): void => {
if (err instanceof AppError) {
res.status(err.statusCode).json({ error: { code: err.code, message: err.message } });
} else {
res.status(500).json({ error: { code: ‘UNKNOWN_ERROR’, message: ‘An unexpected error occurred’ } });
}
};

export const createUserHandler = (req: express.Request, res: express.Response): void => {
pipe(
createUser(req.body),
(task) => task({ db: pool }),
(promise) =>
promise.then((result) =>
result._tag === ‘Left’
? errorHandler(result.left, res)
: res.json({ message: result.right })
)
);
};

export const getUserHandler = (req: express.Request, res: express.Response): void => {
pipe(
getUser(parseInt(req.params.id, 10)),
(task) => task({ db: pool }),
(promise) =>
promise.then((result) =>
result._tag === ‘Left’
? errorHandler(result.left, res)
: res.json(result.right)
)
);
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

Express API (index.ts)

import express from ‘express’;
import { createUserHandler, getUserHandler } from ‘./controllers/UserController’;

const app = express();
app.use(express.json());

// Routes
app.post(‘/users’, createUserHandler);
app.get(‘/users/:id’, getUserHandler);

// Start Server
app.listen(3000, () => {
console.log(‘Server running on http://localhost:3000’);
});

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اجرای برنامه با Docker و Docker Compose

Dockerfile

# Stage 1: Build
FROM node:22 AS builder
WORKDIR /app
COPY package*.json .
RUN npm install
COPY . .
RUN npm run build

# Stage 2: Run
FROM node:22
WORKDIR /app
COPY –from=builder /app/dist ./dist
COPY package*.json ./
RUN npm install –production
CMD [“node”, “dist/index.js”]

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

docker-compose.yml

version: ‘3.8’
services:
db:
image: postgres:15
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: fp_ts_crud
ports:
– “5432:5432”
volumes:
– db_data:/var/lib/postgresql/data
volumes:
db_data:

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برنامه – حالت توسعه را اجرا کنید

# Start the database
docker-compose up -d

# Run the app
npx ts-node-dev src/index.ts

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برنامه – حالت تولید را اجرا کنید

# Build the docker image
docker build -t fp-ts-crud-app .

# Start the database
docker-compose up -d

# Run the container
docker run -p 3000:3000 fp-ts-crud-app

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تست های نوشتاری

راه اندازی بله

به روز رسانی package.json اسکریپت ها:

“scripts”: {
“test”: “jest”,
“test:watch”: “jest –watch”
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

آزمون نمونه (__tests__/UserService.test.ts)

import { createUser, getUser } from ‘../services/UserService’;
import { pool } from ‘../db’;

jest.mock(‘../db’, () => ({
pool: {
query: jest.fn(),
},
}));

describe(‘UserService’, () => {
afterEach(() => {
jest.clearAllMocks();
});

it(‘should create a user’, async () => {
(pool.query as jest.Mock).mockResolvedValueOnce({ rows: [{ id: 1 }] });

const result = await createUser({ name: ‘Alice’, email: ‘alice@example.com’ })({ db: pool })();

expect(result._tag).toBe(‘Right’);
if (result._tag === ‘Right’) {
expect(result.right).toBe(‘User created with ID: 1’);
}
});

it(‘should return error if user not found’, async () => {
(pool.query as jest.Mock).mockResolvedValueOnce({ rows: [] });

const result = await getUser(1)({ db: pool })();

expect(result._tag).toBe(‘Left’);
if (result._tag === ‘Left’) {
expect(result.left.message).toBe(‘User not found’);
}
});

it(‘should return error for invalid user data during creation’, async () => {
const invalidData = { name: 123, email: ‘invalid-email’ };
const result = await createUser(invalidData)({ db: pool })();
expect(result._tag).toBe(‘Left’);

if (result._tag === ‘Left’) {
expect(result.left.message).toBe(‘Invalid user data provided’);
}
});
});

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نتیجه گیری

با اعمال اهرم fp-ts، Docker و مدیریت خطای قوی، ما یک برنامه کاربردی Node.js CRUD، مقیاس پذیر و قابل نگهداری ساختیم. استفاده از الگوهای برنامه نویسی کاربردی کد شما را قابل پیش بینی تر و قابل اعتمادتر می کند، به خصوص در هنگام مدیریت گردش کار ناهمزمان.

مقدمه

برنامه نویسی تابعی (FP) به دلیل ترکیب پذیری، آزمایش پذیری و استحکام آن محبوبیت پیدا کرده است. در اکوسیستم جاوا اسکریپت، کتابخانه ها مانند fp-ts مفاهیم قدرتمند FP را به TypeScript بیاورید و به شما امکان می دهد کدهای تمیزتر و قابل اعتمادتری بنویسید.

این مقاله بررسی می کند fp-ts مفاهیمی مانند Option، Either، Task، Reader، و ReaderTaskEither. ما با استفاده از یک برنامه اولیه CRUD خواهیم ساخت fp-ts، صفحه (کلاینت PostgreSQL)، و Express.js برای دیدن اینکه چگونه این انتزاعات در کاربردهای دنیای واقعی می درخشند.


مفاهیم کلیدی

قبل از ورود به برنامه، اجازه دهید به طور خلاصه در مورد مفاهیم اصلی بحث کنیم:

  1. گزینه: وجود یا عدم وجود یک مقدار را مدل می کند (Some یا None).
  2. هر دو: نشان دهنده محاسباتی است که می توانند موفق شوند (Right) یا شکست بخورد (Left).
  3. وظیفه: نشان دهنده محاسبات ناهمزمان تنبل است.
  4. خواننده: وابستگی ها را به محاسبات تزریق می کند.
  5. ReaderTaskEither: Reader، Task و Either را برای عملیات ناهمگام با وابستگی ها و مدیریت خطا ترکیب می کند.

راه اندازی پروژه

پروژه را راه اندازی کنید

mkdir fp-ts-crud && cd fp-ts-crud
npm init -y
npm install express pg fp-ts io-ts
npm install --save-dev typescript @types/express ts-node-dev jest @types/jest ts-jest
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تنظیم TypeScript

ایجاد یک tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ساختار پروژه

src/
  index.ts        # Entry point
  db.ts           # Database setup
  models/         # Data models and validation
  services/       # Business logic
  controllers/    # CRUD operations
  utils/          # fp-ts utilities
  errors/         # Custom error classes
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید


پیاده سازی برنامه CRUD

راه اندازی پایگاه داده (db.ts)

import { Pool } from 'pg';

export const pool = new Pool({
  user: 'postgres',
  host: 'localhost',
  database: 'fp_ts_crud',
  password: 'password',
  port: 5432,
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تعریف مدل ها و اعتبارسنجی (models/User.ts)

import * as t from 'io-ts';
import { isRight } from 'fp-ts/Either';

export const User = t.type({
  id: t.number,
  name: t.string,
  email: t.string,
});

export const validateUser = (data: unknown): t.TypeOf<typeof User> | null => {
  const result = User.decode(data);
  return isRight(result) ? result.right : null;
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مدیریت خطای سفارشی (errors/AppError.ts)

export class AppError extends Error {
  constructor(public statusCode: number, public code: string, public message: string) {
    super(message);
    this.name = 'AppError';
  }
}

export const createAppError = (statusCode: number, code: string, message: string): AppError => {
  return new AppError(statusCode, code, message);
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

لایه سرویس (services/UserService.ts)

import { pool } from '../db';
import { ReaderTaskEither, right, left } from 'fp-ts/ReaderTaskEither';
import { pipe } from 'fp-ts/function';
import { createAppError, AppError } from '../errors/AppError';
import { validateUser } from '../models/User';

type Dependencies = { db: typeof pool };
type User = { name: string; email: string };

export const createUser = (
  user: User
): ReaderTaskEither<Dependencies, AppError, string> => (deps) => async () => {
  // Validate the incoming user data
  const validatedUser = validateUser(user);

  if (!validatedUser) {
    return left(createAppError(400, 'INVALID_USER_DATA', 'Invalid user data provided'));
  }

  try {
    const result = await deps.db.query(
      'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id',
      [validatedUser.name, validatedUser.email]
    );
    return right(`User created with ID: ${result.rows[0].id}`);
  } catch (error) {
    return left(createAppError(500, 'USER_CREATION_FAILED', 'Failed to create user'));
  }
};

export const getUser = (
  id: number
): ReaderTaskEither<Dependencies, AppError, { id: number; name: string; email: string }> => (deps) => async () => {
  try {
    const result = await deps.db.query('SELECT * FROM users WHERE id = $1', [id]);
    return result.rows[0]
      ? right(result.rows[0])
      : left(createAppError(404, 'USER_NOT_FOUND', 'User not found'));
  } catch {
    return left(createAppError(500, 'USER_FETCH_FAILED', 'Failed to fetch user'));
  }
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

عملیات CRUD (controllers/UserController.ts)

import { pipe } from 'fp-ts/function';
import { createUser, getUser } from '../services/UserService';
import { pool } from '../db';
import { AppError } from '../errors/AppError';

const errorHandler = (err: unknown, res: express.Response): void => {
  if (err instanceof AppError) {
    res.status(err.statusCode).json({ error: { code: err.code, message: err.message } });
  } else {
    res.status(500).json({ error: { code: 'UNKNOWN_ERROR', message: 'An unexpected error occurred' } });
  }
};

export const createUserHandler = (req: express.Request, res: express.Response): void => {
  pipe(
    createUser(req.body),
    (task) => task({ db: pool }),
    (promise) =>
      promise.then((result) =>
        result._tag === 'Left'
          ? errorHandler(result.left, res)
          : res.json({ message: result.right })
      )
  );
};

export const getUserHandler = (req: express.Request, res: express.Response): void => {
  pipe(
    getUser(parseInt(req.params.id, 10)),
    (task) => task({ db: pool }),
    (promise) =>
      promise.then((result) =>
        result._tag === 'Left'
          ? errorHandler(result.left, res)
          : res.json(result.right)
      )
  );
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

Express API (index.ts)

import express from 'express';
import { createUserHandler, getUserHandler } from './controllers/UserController';

const app = express();
app.use(express.json());

// Routes
app.post('/users', createUserHandler);
app.get('/users/:id', getUserHandler);

// Start Server
app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید


اجرای برنامه با Docker و Docker Compose

Dockerfile

# Stage 1: Build
FROM node:22 AS builder
WORKDIR /app
COPY package*.json .
RUN npm install
COPY . .
RUN npm run build

# Stage 2: Run
FROM node:22
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm install --production
CMD ["node", "dist/index.js"]
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

docker-compose.yml

version: '3.8'
services:
  db:
    image: postgres:15
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: fp_ts_crud
    ports:
      - "5432:5432"
    volumes:
      - db_data:/var/lib/postgresql/data
volumes:
  db_data:
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برنامه – حالت توسعه را اجرا کنید

# Start the database
docker-compose up -d

# Run the app
npx ts-node-dev src/index.ts
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برنامه – حالت تولید را اجرا کنید

# Build the docker image
docker build -t fp-ts-crud-app .

# Start the database
docker-compose up -d

# Run the container
docker run -p 3000:3000 fp-ts-crud-app
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید


تست های نوشتاری

راه اندازی بله

به روز رسانی package.json اسکریپت ها:

"scripts": {
  "test": "jest",
  "test:watch": "jest --watch"
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

آزمون نمونه (__tests__/UserService.test.ts)

import { createUser, getUser } from '../services/UserService';
import { pool } from '../db';

jest.mock('../db', () => ({
  pool: {
    query: jest.fn(),
  },
}));

describe('UserService', () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should create a user', async () => {
    (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [{ id: 1 }] });

    const result = await createUser({ name: 'Alice', email: 'alice@example.com' })({ db: pool })();

    expect(result._tag).toBe('Right');
    if (result._tag === 'Right') {
      expect(result.right).toBe('User created with ID: 1');
    }
  });

  it('should return error if user not found', async () => {
    (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [] });

    const result = await getUser(1)({ db: pool })();

    expect(result._tag).toBe('Left');
    if (result._tag === 'Left') {
      expect(result.left.message).toBe('User not found');
    }
  });

  it('should return error for invalid user data during creation', async () => {
    const invalidData = { name: 123, email: 'invalid-email' };
    const result = await createUser(invalidData)({ db: pool })();
    expect(result._tag).toBe('Left');

    if (result._tag === 'Left') {
      expect(result.left.message).toBe('Invalid user data provided');
    }
  });
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید


نتیجه گیری

با اعمال اهرم fp-ts، Docker و مدیریت خطای قوی، ما یک برنامه کاربردی Node.js CRUD، مقیاس پذیر و قابل نگهداری ساختیم. استفاده از الگوهای برنامه نویسی کاربردی کد شما را قابل پیش بینی تر و قابل اعتمادتر می کند، به خصوص در هنگام مدیریت گردش کار ناهمزمان.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا