Node.js / Backend

Jak zbudować RESTful API w Node.js

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.

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;
💡 Tip: Używaj 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);
  }
};
⚠️ Security: Nigdy nie commituj 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