RESTful API to fundament nowoczesnych aplikacji webowych. W tym przewodniku pokażę jak krok po kroku zbudować production-ready API w Node.js z użyciem Express, middleware, validation, authentication i dobrych praktyk security.
Spis treści
- 1. Setup projektu (Express + TypeScript)
- 2. Routing i REST conventions
- 3. Middleware (logging, CORS, security)
- 4. Input validation (Zod)
- 5. Centralized error handling
- 6. Database integration (Prisma)
- 7. Authentication & Authorization (JWT)
- 8. Rate limiting & DDoS protection
- 9. Testing (Vitest + Supertest)
- 10. Deployment (Docker + Railway)
1. Setup projektu
Zacznijmy od zainicjowania projektu z TypeScript:
# Inicjalizacja projektu
mkdir my-api && cd my-api
npm init -y
# Instalacja zależności
npm install express cors helmet compression
npm install -D typescript @types/express @types/node tsx
# TypeScript config
npx tsc --init
tsconfig.json (recommended)
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Struktura folderów
src/
├── server.ts # Entry point
├── app.ts # Express app config
├── routes/
│ ├── index.ts
│ ├── users.routes.ts
│ └── products.routes.ts
├── controllers/
│ ├── users.controller.ts
│ └── products.controller.ts
├── middleware/
│ ├── errorHandler.ts
│ ├── validate.ts
│ └── auth.ts
├── services/
│ ├── users.service.ts
│ └── products.service.ts
├── utils/
│ └── logger.ts
└── types/
└── index.ts
2. Routing i REST Conventions
REST API powinno być predictable. Standardowe konwencje HTTP:
| Method | Endpoint | Action |
|---|---|---|
GET |
/api/products |
Lista wszystkich produktów |
GET |
/api/products/:id |
Pojedynczy produkt |
POST |
/api/products |
Utwórz nowy produkt |
PUT/PATCH |
/api/products/:id |
Zaktualizuj produkt |
DELETE |
/api/products/:id |
Usuń produkt |
Przykład: Products Router
// routes/products.routes.ts
import { Router } from 'express';
import * as productsController from '../controllers/products.controller';
import { authenticate } from '../middleware/auth';
import { validate } from '../middleware/validate';
import { createProductSchema } from '../schemas/product.schema';
const router = Router();
router.get('/', productsController.getAllProducts);
router.get('/:id', productsController.getProductById);
// Protected routes (require authentication)
router.post(
'/',
authenticate,
validate(createProductSchema),
productsController.createProduct
);
router.patch('/:id', authenticate, productsController.updateProduct);
router.delete('/:id', authenticate, productsController.deleteProduct);
export default router;
3. Middleware Stack
Middleware to funkcje pomiędzy request a response. Kolejność ma znaczenie!
// app.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import compression from 'compression';
import { errorHandler } from './middleware/errorHandler';
import routes from './routes';
const app = express();
// 1. Security headers
app.use(helmet());
// 2. CORS (configure for production)
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
credentials: true,
}));
// 3. Compression (gzip responses)
app.use(compression());
// 4. Body parsers
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// 5. Logging (custom middleware)
app.use((req, res, next) => {
console.log(`${req.method} ${req.path} - ${new Date().toISOString()}`);
next();
});
// 6. Routes
app.use('/api', routes);
// 7. Error handler (must be last!)
app.use(errorHandler);
export default app;
helmet() zawsze w production. Dodaje security headers
(X-Content-Type-Options, X-Frame-Options, etc.) przeciwko common attacks.
4. Input Validation (Zod)
Nigdy nie ufaj danych od użytkownika. Waliduj wszystko.
# Instalacja Zod
npm install zod
Schema Definition
// schemas/product.schema.ts
import { z } from 'zod';
export const createProductSchema = z.object({
body: z.object({
name: z.string().min(3).max(100),
price: z.number().positive(),
category: z.enum(['electronics', 'clothing', 'food']),
description: z.string().optional(),
inStock: z.boolean().default(true),
}),
});
export type CreateProductInput = z.infer<typeof createProductSchema>;
Validation Middleware
// middleware/validate.ts
import { Request, Response, NextFunction } from 'express';
import { AnyZodObject, ZodError } from 'zod';
export const validate = (schema: AnyZodObject) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
});
next();
} catch (error) {
if (error instanceof ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors,
});
}
next(error);
}
};
};
5. Centralized Error Handling
Jedna funkcja do obsługi wszystkich błędów. DRY principle.
// middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
export class AppError extends Error {
statusCode: number;
isOperational: boolean;
constructor(message: string, statusCode: number = 500) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
export const errorHandler = (
err: Error | AppError,
req: Request,
res: Response,
next: NextFunction
) => {
// Default to 500 server error
let statusCode = 500;
let message = 'Internal Server Error';
if (err instanceof AppError) {
statusCode = err.statusCode;
message = err.message;
}
// Log errors (use logger in production)
console.error(`[ERROR] ${message}`, err.stack);
// Send error response
res.status(statusCode).json({
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};
Używanie w Controller
// controllers/products.controller.ts
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../middleware/errorHandler';
export const getProductById = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const product = await productService.findById(req.params.id);
if (!product) {
throw new AppError('Product not found', 404);
}
res.json(product);
} catch (error) {
next(error); // Pass to error handler
}
};
6. Database Integration (Prisma)
Prisma to modern ORM z type-safety i migrациami out of the box.
# Setup Prisma
npm install prisma @prisma/client
npx prisma init
# Generate Prisma Client (po zdefiniowaniu schema)
npx prisma generate
# Run migrations
npx prisma migrate dev
Prisma Schema Example
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Product {
id String @id @default(uuid())
name String
price Float
category String
description String?
inStock Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Service Layer (Business Logic)
// services/products.service.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const findAll = async () => {
return prisma.product.findMany({
orderBy: { createdAt: 'desc' },
});
};
export const findById = async (id: string) => {
return prisma.product.findUnique({ where: { id } });
};
export const create = async (data: CreateProductInput['body']) => {
return prisma.product.create({ data });
};
export const update = async (id: string, data: Partial<Product>) => {
return prisma.product.update({ where: { id }, data });
};
export const remove = async (id: string) => {
return prisma.product.delete({ where: { id } });
};
7. Authentication (JWT)
JSON Web Tokens to standard dla stateless authentication.
npm install jsonwebtoken bcrypt
npm install -D @types/jsonwebtoken @types/bcrypt
JWT Middleware
// middleware/auth.ts
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import { AppError } from './errorHandler';
interface JWTPayload {
userId: string;
email: string;
}
export const authenticate = (
req: Request,
res: Response,
next: NextFunction
) => {
const token = req.headers.authorization?.split(' ')[1]; // Bearer TOKEN
if (!token) {
throw new AppError('No token provided', 401);
}
try {
const decoded = jwt.verify(
token,
process.env.JWT_SECRET!
) as JWTPayload;
req.user = decoded; // Attach user to request
next();
} catch (error) {
throw new AppError('Invalid token', 401);
}
};
JWT_SECRET do repozytorium.
Użyj .env i dodaj do .gitignore.
8. Rate Limiting
Ochrona przed brute-force attacks i DDoS.
npm install express-rate-limit
// middleware/rateLimit.ts
import rateLimit from 'express-rate-limit';
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minut
max: 100, // max 100 requests per windowMs
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // max 5 login attempts
message: 'Too many login attempts, try again later',
});
// Usage in app.ts
app.use('/api', apiLimiter);
app.use('/api/auth/login', authLimiter);
9. Testing (Vitest + Supertest)
npm install -D vitest supertest @types/supertest
Integration Test Example
// __tests__/products.test.ts
import { describe, it, expect, beforeAll } from 'vitest';
import request from 'supertest';
import app from '../src/app';
describe('Products API', () => {
let authToken: string;
beforeAll(async () => {
// Login to get auth token
const res = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password' });
authToken = res.body.token;
});
it('GET /api/products - should return all products', async () => {
const res = await request(app)
.get('/api/products')
.expect(200);
expect(res.body).toBeInstanceOf(Array);
});
it('POST /api/products - should create new product', async () => {
const newProduct = {
name: 'Test Product',
price: 99.99,
category: 'electronics',
};
const res = await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${authToken}`)
.send(newProduct)
.expect(201);
expect(res.body).toHaveProperty('id');
expect(res.body.name).toBe('Test Product');
});
it('POST /api/products - should fail without auth', async () => {
await request(app)
.post('/api/products')
.send({ name: 'Test' })
.expect(401);
});
});
10. Deployment
Przykład deployment na Railway (podobnie Render, Fly.io):
Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npx prisma generate
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Environment Variables (Railway)
DATABASE_URL=postgresql://...
JWT_SECRET=your-super-secret-key
NODE_ENV=production
PORT=3000
ALLOWED_ORIGINS=https://yourapp.com
Podsumowanie
Kluczowe takeaways:
- TypeScript – type safety od początku
- Middleware stack – security, validation, error handling
- Zod validation – nigdy nie ufaj input
- Centralized errors – jeden handler dla całej aplikacji
- JWT auth – stateless authentication
- Rate limiting – ochrona przed abuse
- Testing – integration tests z Supertest
Pełny kod źródłowy: Dostępny na GitHub
Pytania? Napisz w komentarzach lub skontaktuj się — chętnie pomogę!
Podobał Ci się artykuł?
Zapisz się do newslettera — raz w miesiącu dostaniesz podobne treści + ekskluzywne case studies.
Zapisz się do newslettera