在 Node.js 中使用 fp-ts 进行函数式编程
介绍
函数式编程 (FP) 因其可组合性、可测试性和稳健性而广受欢迎。在 JavaScript 生态系统中,**fp-ts** 等库将强大的 FP 概念引入 TypeScript,让您可以编写更简洁、更可靠的代码。
本文探讨了 **fp-ts** 概念,例如 `Option`、`Either`、`Task`、`Reader` 和 `ReaderTaskEither`。我们将使用 **fp-ts**、**pg**(PostgreSQL 客户端)和 **Express.js** 构建一个基本的 CRUD 应用程序,以了解这些抽象在实际应用程序中的表现。
关键概念
在深入研究该应用程序之前,让我们简单讨论一下主要概念:
设置项目
初始化项目
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| 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=> (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 => (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
编写测试
设置 Jest
更新 `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 应用程序。使用函数式编程模式可让您的代码更加可预测和可靠,尤其是在处理异步工作流时。