在 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 应用程序。使用函数式编程模式可让您的代码更加可预测和可靠,尤其是在处理异步工作流时。