פיתוח API מקצועי: המדריך השלם ל-2025
API טוב הוא הלב של כל אפליקציה מודרנית. במדריך זה נלמד איך לבנות API מאובטח, מהיר וקל לתחזוקה שישרת את המשתמשים שלכם לשנים.
עקרונות יסוד בעיצוב API
1. RESTful Design Principles
מבנה URLs נכון
✅ נכון:
GET /api/v1/users - רשימת משתמשים
GET /api/v1/users/123 - משתמש ספציפי
POST /api/v1/users - יצירת משתמש
PUT /api/v1/users/123 - עדכון מלא
PATCH /api/v1/users/123 - עדכון חלקי
DELETE /api/v1/users/123 - מחיקה
❌ לא נכון:
GET /api/getUsers
POST /api/createNewUser
GET /api/user_by_id?id=123
2. שימוש נכון ב-HTTP Status Codes
// Node.js + Express דוגמה
app.post('/api/users', async (req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json({
success: true,
data: user,
message: 'User created successfully'
});
} catch (error) {
if (error.code === 11000) {
// Duplicate key error
return res.status(409).json({
success: false,
error: 'User already exists',
code: 'DUPLICATE_USER'
});
}
if (error.name === 'ValidationError') {
return res.status(400).json({
success: false,
error: 'Invalid input data',
details: error.errors
});
}
// Server error
res.status(500).json({
success: false,
error: 'Internal server error',
code: 'SERVER_ERROR'
});
}
});
קודי תגובה חשובים
| קוד | משמעות | שימוש |
|---|---|---|
| 200 | OK | בקשה הצליחה |
| 201 | Created | משאב נוצר בהצלחה |
| 204 | No Content | הצלחה ללא תוכן (DELETE) |
| 400 | Bad Request | קלט לא תקין |
| 401 | Unauthorized | דרושה הזדהות |
| 403 | Forbidden | אין הרשאה |
| 404 | Not Found | משאב לא נמצא |
| 409 | Conflict | קונפליקט (כפילות) |
| 422 | Unprocessable Entity | ולידציה נכשלה |
| 429 | Too Many Requests | Rate limit |
| 500 | Internal Server Error | שגיאת שרת |
| 503 | Service Unavailable | שירות לא זמין |
אבטחת API
1. Authentication & Authorization
JWT Implementation
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
// יצירת Token
function generateToken(user) {
return jwt.sign(
{
slug: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{
expiresIn: '1h',
issuer: 'api.example.com',
audience: 'example.com'
}
);
}
// Middleware לאימות
const authenticate = async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id).select('-password');
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(403).json({ error: 'Invalid token' });
},
image: '/blog/api-development-best-practices.jpg',
imageAlt: 'תמונת נושא',
keyPoints: [
'נקודה חשובה 1',
'נקודה חשובה 2',
'נקודה חשובה 3'
]
};
// Middleware להרשאות
const authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: 'Insufficient permissions'
});
}
next();
};
};
// שימוש
app.get('/api/admin/users',
authenticate,
authorize('admin', 'super-admin'),
getUsers
);
2. Rate Limiting
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
// Rate limiter עם Redis
const createLimiter = (options) => {
return rateLimit({
store: new RedisStore({
client: new Redis({
host: process.env.REDIS_HOST,
port: 6379
})
}),
windowMs: options.windowMs || 15 * 60 * 1000, // 15 דקות
max: options.max || 100,
message: 'Too many requests from this IP',
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: req.rateLimit.resetTime
});
}
});
};
// הגבלות שונות לפי endpoint
app.use('/api/auth/login', createLimiter({
windowMs: 15 * 60 * 1000,
max: 5
}));
app.use('/api/auth/register', createLimiter({
windowMs: 60 * 60 * 1000,
max: 3
}));
app.use('/api', createLimiter({
windowMs: 15 * 60 * 1000,
max: 100
}));
3. Input Validation & Sanitization
const { body, validationResult } = require('express-validator');
const DOMPurify = require('isomorphic-dompurify');
// Validation middleware
const validateUser = [
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Invalid email format'),
body('password')
.isLength({ min: 8 })
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[@$!%*?&])[A-Za-zd@$!%*?&]/)
.withMessage('Password must contain uppercase, lowercase, number and special character'),
body('name')
.trim()
.isLength({ min: 2, max: 50 })
.matches(/^[a-zA-Zא-תs]+$/)
.withMessage('Name can only contain letters'),
body('phone')
.optional()
.isMobilePhone('he-IL')
.withMessage('Invalid Israeli phone number'),
body('bio')
.optional()
.customSanitizer(value => DOMPurify.sanitize(value))
];
// Error handling
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({
success: false,
errors: errors.array().map(err => ({
field: err.param,
message: err.msg
}))
});
}
next();
};
app.post('/api/users',
validateUser,
handleValidationErrors,
createUser
);
4. SQL Injection Prevention
// ❌ פגיע ל-SQL Injection
app.get('/api/users', (req, res) => {
const query = `SELECT * FROM users WHERE name = '${req.query.name}'`;
db.query(query); // מסוכן!
});
// ✅ מוגן עם Parameterized Queries
app.get('/api/users', async (req, res) => {
const query = 'SELECT * FROM users WHERE name = ?';
const [rows] = await db.execute(query, [req.query.name]);
res.json(rows);
});
// עם Sequelize ORM
const users = await User.findAll({
where: {
name: req.query.name, // Sequelize מטפל באבטחה
[Op.or]: [
{ status: 'active' },
{ role: 'admin' }
]
}
});
ביצועים ואופטימיזציה
1. Caching Strategy
const redis = require('redis');
const client = redis.createClient();
// Cache middleware
const cache = (duration = 300) => {
return async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
try {
const cached = await client.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
} catch (error) {
console.error('Cache error:', error);
}
// Store original send function
const originalSend = res.json;
// Override send function
res.json = function(data) {
// Cache the response
client.setex(key, duration, JSON.stringify(data));
// Call original send
originalSend.call(this, data);
};
next();
};
};
// Cache invalidation
const invalidateCache = async (pattern) => {
const keys = await client.keys(`cache:${pattern}*`);
if (keys.length) {
await client.del(keys);
}
};
// שימוש
app.get('/api/products', cache(600), getProducts);
app.post('/api/products', async (req, res) => {
const product = await createProduct(req.body);
await invalidateCache('/api/products');
res.status(201).json(product);
});
2. Database Query Optimization
// ❌ N+1 Problem
const posts = await Post.findAll();
for (const post of posts) {
post.comments = await Comment.findAll({
where: { postId: post.id }
});
}
// ✅ Eager Loading
const posts = await Post.findAll({
include: [
{
model: Comment,
as: 'comments',
include: [
{
model: User,
as: 'author',
attributes: ['id', 'name', 'avatar']
}
]
}
],
limit: 20,
order: [['createdAt', 'DESC']]
});
3. Pagination
const paginate = (model) => {
return async (req, res, next) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const offset = (page - 1) * limit;
try {
const { count, rows } = await model.findAndCountAll({
limit,
offset,
order: [['createdAt', 'DESC']]
});
res.paginatedResults = {
data: rows,
pagination: {
total: count,
page,
limit,
pages: Math.ceil(count / limit),
hasNext: page < Math.ceil(count / limit),
hasPrev: page > 1
}
};
next();
} catch (error) {
res.status(500).json({ error: error.message });
}
};
};
// שימוש
app.get('/api/products', paginate(Product), (req, res) => {
res.json(res.paginatedResults);
});
GraphQL Best Practices
Schema Design
# schema.graphql
type User {
slug: ID!
email: String!
name: String!
posts: [Post!]! @relation
createdAt: DateTime!
}
type Post {
slug: ID!
title: String!
content: String!
author: User! @relation
comments: [Comment!]! @relation
likes: Int!
published: Boolean!
}
type Query {
user(id: ID!): User
users(
filter: UserFilter
sort: UserSort
limit: Int = 20
offset: Int = 0
): UserConnection!
post(id: ID!): Post
posts(
filter: PostFilter
sort: PostSort
limit: Int = 20
offset: Int = 0
): PostConnection!
}
type Mutation {
createUser(input: CreateUserInput!): UserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UserPayload!
deleteUser(id: ID!): DeletePayload!
createPost(input: CreatePostInput!): PostPayload!
publishPost(id: ID!): PostPayload!
}
# Pagination
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
Resolver Implementation
const { GraphQLError } = require('graphql');
const resolvers = {
Query: {
users: async (parent, args, context) => {
// Check authentication
if (!context.user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' }
});
}
const { filter, sort, limit, offset } = args;
// Build query
const query = {};
if (filter?.name) {
query.name = { $regex: filter.name, $options: 'i' };
}
// Execute with DataLoader for N+1 prevention
const users = await context.dataloaders.users.loadMany(
await User.find(query)
.sort(sort || { createdAt: -1 })
.limit(limit)
.skip(offset)
.select('_id')
);
const total = await User.countDocuments(query);
return {
edges: users.map((user, index) => ({
node: user,
cursor: Buffer.from(`${offset + index}`).toString('base64')
})),
pageInfo: {
hasNextPage: offset + limit < total,
hasPreviousPage: offset > 0
},
totalCount: total
};
}
},
User: {
posts: async (parent, args, context) => {
return context.dataloaders.postsByUser.load(parent.id);
}
},
Mutation: {
createUser: async (parent, { input }, context) => {
// Validation
const errors = validateUserInput(input);
if (errors.length) {
throw new GraphQLError('Invalid input', {
extensions: {
code: 'BAD_USER_INPUT',
errors
}
});
}
try {
const user = await User.create(input);
// Publish to subscriptions
context.pubsub.publish('USER_CREATED', {
userCreated: user
});
return {
success: true,
user,
message: 'User created successfully'
};
} catch (error) {
throw new GraphQLError('Failed to create user', {
extensions: {
code: 'INTERNAL_SERVER_ERROR',
error: error.message
}
});
}
}
}
};
API Documentation
1. OpenAPI/Swagger
# openapi.yaml
openapi: 3.0.0
info:
title: BigDesign API
version: 1.0.0
description: API for web development services
contact:
email: [email protected]
servers:
- url: https://api.bigdesign.co.il/v1
description: Production
- url: https://staging-api.bigdesign.co.il/v1
description: Staging
paths:
/users:
get:
summary: Get all users
tags:
- Users
parameters:
- in: query
name: page
schema:
type: integer
default: 1
- in: query
name: limit
schema:
type: integer
default: 20
maximum: 100
responses:
200:
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/UserList'
401:
$ref: '#/components/responses/Unauthorized'
post:
summary: Create new user
tags:
- Users
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUser'
responses:
201:
description: User created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
slug:
type: string
format: uuid
email:
type: string
format: email
name:
type: string
createdAt:
type: string
format: date-time
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
2. Postman Collection
{
"info": {
"name": "BigDesign API",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{access_token}}",
"type": "string"
}
]
},
"item": [
{
"name": "Authentication",
"item": [
{
"name": "Login",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status code is 200', () => {",
" pm.response.to.have.status(200);",
"});",
"",
"const response = pm.response.json();",
"pm.environment.set('access_token', response.token);"
]
}
}
],
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"email\": \"[email protected]\",\n \"password\": \"Test123!@#\"\n}"
},
"url": "{{base_url}}/auth/login"
}
}
]
}
]
}
Testing
Unit Tests
const request = require('supertest');
const app = require('../app');
describe('User API', () => {
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
email: '[email protected]',
password: 'Test123!@#',
name: 'Test User'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body).toHaveProperty('data');
expect(response.body.data.email).toBe(userData.email);
});
it('should return 422 for invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'invalid-email',
password: 'Test123!@#',
name: 'Test User'
})
.expect(422);
expect(response.body.errors[0].field).toBe('email');
});
});
});
Integration Tests
describe('User Flow Integration', () => {
let token;
let userId;
beforeAll(async () => {
await db.connect();
});
afterAll(async () => {
await db.close();
});
it('complete user flow', async () => {
// 1. Register
const registerRes = await request(app)
.post('/api/auth/register')
.send({
email: '[email protected]',
password: 'Test123!@#',
name: 'Integration Test'
})
.expect(201);
userId = registerRes.body.data.id;
// 2. Login
const loginRes = await request(app)
.post('/api/auth/login')
.send({
email: '[email protected]',
password: 'Test123!@#'
})
.expect(200);
token = loginRes.body.token;
// 3. Get Profile
const profileRes = await request(app)
.get('/api/users/me')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(profileRes.body.data.email).toBe('[email protected]');
// 4. Update Profile
await request(app)
.patch(`/api/users/${userId}`)
.set('Authorization', `Bearer ${token}`)
.send({ name: 'Updated Name' })
.expect(200);
// 5. Delete Account
await request(app)
.delete(`/api/users/${userId}`)
.set('Authorization', `Bearer ${token}`)
.expect(204);
});
});
Monitoring & Logging
Structured Logging
const winston = require('winston');
const { ElasticsearchTransport } = require('winston-elasticsearch');
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: {
service: 'api',
environment: process.env.NODE_ENV
},
transports: [
new winston.transports.Console({
format: winston.format.simple()
}),
new ElasticsearchTransport({
level: 'info',
clientOpts: {
node: process.env.ELASTICSEARCH_URL
},
index: 'api-logs'
})
]
});
// Request logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('API Request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration,
ip: req.ip,
userAgent: req.get('user-agent'),
userId: req.user?.id
});
});
next();
});
סיכום
פיתוח API מקצועי דורש תשומת לב לפרטים רבים:
- ארכיטקטורה נכונה ועקבית
- אבטחה מקיפה
- ביצועים מעולים
- תיעוד ברור
- בדיקות יסודיות
- ניטור ולוגינג
השקעה בתשתית API טובה תחסוך זמן ומשאבים רבים בעתיד ותאפשר scale אמיתי של המוצר.