Zum Inhalt springen
TypeScript Fortgeschritten 45 min

Projekt: Typisierte REST API

Baue eine vollständig typisierte REST API mit TypeScript, Express und Zod.

Aktualisiert:

Projekt: Typisierte REST API

In diesem Projekt bauen wir eine vollständig typisierte REST API. Du lernst, wie man TypeScript effektiv in einem Backend-Projekt einsetzt.

Projekt Setup

mkdir ts-api-projekt
cd ts-api-projekt
npm init -y
npm install express cors
npm install -D typescript @types/express @types/cors @types/node ts-node nodemon

TypeScript Konfiguration

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

Package.json Scripts

{
  "scripts": {
    "dev": "nodemon --exec ts-node src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Projekt Struktur

src/
├── index.ts
├── types/
│   ├── user.ts
│   └── api.ts
├── routes/
│   └── users.ts
├── services/
│   └── userService.ts
└── middleware/
    └── errorHandler.ts

Typ-Definitionen

src/types/user.ts

// User Entity
export interface User {
    id: string;
    name: string;
    email: string;
    role: "admin" | "user" | "guest";
    createdAt: Date;
    updatedAt: Date;
}

// Für neue User (ohne id und Timestamps)
export type CreateUserInput = Omit<User, "id" | "createdAt" | "updatedAt">;

// Für Updates (alles optional)
export type UpdateUserInput = Partial<CreateUserInput>;

// Für API Responses (ohne sensible Daten falls nötig)
export type UserResponse = User;

// Für Listen
export type UserListItem = Pick<User, "id" | "name" | "email" | "role">;

src/types/api.ts

// Generische API Response
export interface ApiResponse<T> {
    success: boolean;
    data?: T;
    error?: ApiError;
    meta?: PaginationMeta;
}

export interface ApiError {
    code: string;
    message: string;
    details?: unknown;
}

export interface PaginationMeta {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
}

// Request Query Parameter
export interface PaginationQuery {
    page?: string;
    limit?: string;
}

export interface UserFilters extends PaginationQuery {
    role?: string;
    search?: string;
}

// Helper Type für typisierte Request Handler
import { Request, Response, NextFunction } from "express";

export type TypedRequestBody<T> = Request<{}, {}, T>;
export type TypedRequestParams<T> = Request<T>;
export type TypedRequestQuery<T> = Request<{}, {}, {}, T>;

export type AsyncHandler<
    Params = {},
    ResBody = {},
    ReqBody = {},
    Query = {}
> = (
    req: Request<Params, ResBody, ReqBody, Query>,
    res: Response<ResBody>,
    next: NextFunction
) => Promise<void>;

User Service

src/services/userService.ts

import { User, CreateUserInput, UpdateUserInput, UserListItem } from "../types/user";
import { PaginationMeta, UserFilters } from "../types/api";

// In-Memory Storage (in Produktion: Datenbank)
const users: Map<string, User> = new Map();

// Helper für ID-Generierung
function generateId(): string {
    return Math.random().toString(36).substring(2, 15);
}

// Service Interface
export interface IUserService {
    findAll(filters: UserFilters): Promise<{ users: UserListItem[]; meta: PaginationMeta }>;
    findById(id: string): Promise<User | null>;
    create(input: CreateUserInput): Promise<User>;
    update(id: string, input: UpdateUserInput): Promise<User | null>;
    delete(id: string): Promise<boolean>;
}

class UserService implements IUserService {
    async findAll(filters: UserFilters): Promise<{ users: UserListItem[]; meta: PaginationMeta }> {
        const page = parseInt(filters.page || "1", 10);
        const limit = parseInt(filters.limit || "10", 10);

        let userArray = Array.from(users.values());

        // Filter by role
        if (filters.role) {
            userArray = userArray.filter(u => u.role === filters.role);
        }

        // Search by name or email
        if (filters.search) {
            const search = filters.search.toLowerCase();
            userArray = userArray.filter(
                u => u.name.toLowerCase().includes(search) ||
                     u.email.toLowerCase().includes(search)
            );
        }

        const total = userArray.length;
        const totalPages = Math.ceil(total / limit);
        const start = (page - 1) * limit;
        const paginatedUsers = userArray.slice(start, start + limit);

        return {
            users: paginatedUsers.map(u => ({
                id: u.id,
                name: u.name,
                email: u.email,
                role: u.role
            })),
            meta: { page, limit, total, totalPages }
        };
    }

    async findById(id: string): Promise<User | null> {
        return users.get(id) || null;
    }

    async create(input: CreateUserInput): Promise<User> {
        const now = new Date();
        const user: User = {
            id: generateId(),
            ...input,
            createdAt: now,
            updatedAt: now
        };
        users.set(user.id, user);
        return user;
    }

    async update(id: string, input: UpdateUserInput): Promise<User | null> {
        const existing = users.get(id);
        if (!existing) return null;

        const updated: User = {
            ...existing,
            ...input,
            updatedAt: new Date()
        };
        users.set(id, updated);
        return updated;
    }

    async delete(id: string): Promise<boolean> {
        return users.delete(id);
    }
}

export const userService = new UserService();

Error Handler Middleware

src/middleware/errorHandler.ts

import { Request, Response, NextFunction } from "express";
import { ApiResponse, ApiError } from "../types/api";

export class AppError extends Error {
    constructor(
        public statusCode: number,
        public code: string,
        message: string,
        public details?: unknown
    ) {
        super(message);
        this.name = "AppError";
    }
}

export function notFound(message = "Resource not found"): never {
    throw new AppError(404, "NOT_FOUND", message);
}

export function badRequest(message: string, details?: unknown): never {
    throw new AppError(400, "BAD_REQUEST", message, details);
}

export function errorHandler(
    err: Error,
    _req: Request,
    res: Response<ApiResponse<never>>,
    _next: NextFunction
): void {
    console.error("Error:", err);

    if (err instanceof AppError) {
        res.status(err.statusCode).json({
            success: false,
            error: {
                code: err.code,
                message: err.message,
                details: err.details
            }
        });
        return;
    }

    res.status(500).json({
        success: false,
        error: {
            code: "INTERNAL_ERROR",
            message: "An unexpected error occurred"
        }
    });
}

Routes

src/routes/users.ts

import { Router, Request, Response, NextFunction } from "express";
import { userService } from "../services/userService";
import { ApiResponse, UserFilters } from "../types/api";
import { User, UserListItem, CreateUserInput, UpdateUserInput } from "../types/user";
import { notFound, badRequest } from "../middleware/errorHandler";

const router = Router();

// Helper für async Handler
const asyncHandler = <T>(
    fn: (req: Request, res: Response<ApiResponse<T>>, next: NextFunction) => Promise<void>
) => (req: Request, res: Response<ApiResponse<T>>, next: NextFunction) => {
    Promise.resolve(fn(req, res, next)).catch(next);
};

// GET /users
router.get(
    "/",
    asyncHandler<{ users: UserListItem[]; meta: any }>(async (req, res) => {
        const filters: UserFilters = {
            page: req.query.page as string,
            limit: req.query.limit as string,
            role: req.query.role as string,
            search: req.query.search as string
        };

        const result = await userService.findAll(filters);

        res.json({
            success: true,
            data: result.users,
            meta: result.meta
        });
    })
);

// GET /users/:id
router.get(
    "/:id",
    asyncHandler<User>(async (req, res) => {
        const user = await userService.findById(req.params.id);

        if (!user) {
            notFound("User not found");
        }

        res.json({
            success: true,
            data: user
        });
    })
);

// POST /users
router.post(
    "/",
    asyncHandler<User>(async (req, res) => {
        const input: CreateUserInput = req.body;

        // Validierung
        if (!input.name || input.name.length < 2) {
            badRequest("Name must be at least 2 characters");
        }
        if (!input.email || !input.email.includes("@")) {
            badRequest("Invalid email address");
        }
        if (!["admin", "user", "guest"].includes(input.role)) {
            badRequest("Role must be admin, user, or guest");
        }

        const user = await userService.create(input);

        res.status(201).json({
            success: true,
            data: user
        });
    })
);

// PUT /users/:id
router.put(
    "/:id",
    asyncHandler<User>(async (req, res) => {
        const input: UpdateUserInput = req.body;
        const user = await userService.update(req.params.id, input);

        if (!user) {
            notFound("User not found");
        }

        res.json({
            success: true,
            data: user
        });
    })
);

// DELETE /users/:id
router.delete(
    "/:id",
    asyncHandler<{ deleted: boolean }>(async (req, res) => {
        const deleted = await userService.delete(req.params.id);

        if (!deleted) {
            notFound("User not found");
        }

        res.json({
            success: true,
            data: { deleted: true }
        });
    })
);

export default router;

Main App

src/index.ts

import express from "express";
import cors from "cors";
import userRoutes from "./routes/users";
import { errorHandler } from "./middleware/errorHandler";

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(cors());
app.use(express.json());

// Routes
app.use("/api/users", userRoutes);

// Health Check
app.get("/health", (_req, res) => {
    res.json({ status: "ok", timestamp: new Date().toISOString() });
});

// Error Handler (muss am Ende stehen)
app.use(errorHandler);

// Server starten
app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});

API testen

Starte den Server mit npm run dev und teste die Endpoints:

# User erstellen
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Max","email":"max@example.com","role":"user"}'

# Alle User abrufen
curl http://localhost:3000/api/users

# Mit Pagination
curl "http://localhost:3000/api/users?page=1&limit=5"

# Mit Filter
curl "http://localhost:3000/api/users?role=admin&search=max"

# User abrufen
curl http://localhost:3000/api/users/{id}

# User aktualisieren
curl -X PUT http://localhost:3000/api/users/{id} \
  -H "Content-Type: application/json" \
  -d '{"name":"Max Updated"}'

# User löschen
curl -X DELETE http://localhost:3000/api/users/{id}

Was du gelernt hast

  • Typisierte Request/Response mit generischen Types
  • Service Pattern mit Interface-Definition
  • Error Handling mit Custom Error Classes
  • Async Handler für typsichere Route Handler
  • Input/Output Types für CRUD Operations

Im nächsten Projekt integrieren wir TypeScript mit React!

Zurück zum TypeScript Kurs