Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungI upgraded three Flutter apps to API 36. Here is what actually broke(20.09.2026 um 17:37 Uhr)
Sichere ProgrammierungSite-Wide Structured Data and Social Graph Integration(20.09.2026 um 17:44 Uhr)
Sichere ProgrammierungExact Match Or Fuzzy Logic For OFAC? 1,400 Tests Changed My Mind.(20.09.2026 um 17:45 Uhr)
Sichere ProgrammierungWhat Makes You Trust an APK Download?(20.09.2026 um 17:47 Uhr)
Sichere ProgrammierungGood News For Backend And Devops Buddy(20.09.2026 um 17:50 Uhr)
Linux Tipps & HardeningBeginner Guide: What Is Linux Mint and Why It Is So Popular(20.09.2026 um 17:54 Uhr)
Sichere ProgrammierungI upgraded three Flutter apps to API 36. Here is what actually broke(20.09.2026 um 17:37 Uhr)
Sichere ProgrammierungSite-Wide Structured Data and Social Graph Integration(20.09.2026 um 17:44 Uhr)
Sichere ProgrammierungExact Match Or Fuzzy Logic For OFAC? 1,400 Tests Changed My Mind.(20.09.2026 um 17:45 Uhr)
Sichere ProgrammierungWhat Makes You Trust an APK Download?(20.09.2026 um 17:47 Uhr)
Sichere ProgrammierungGood News For Backend And Devops Buddy(20.09.2026 um 17:50 Uhr)
Linux Tipps & HardeningBeginner Guide: What Is Linux Mint and Why It Is So Popular(20.09.2026 um 17:54 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Symfony 7: Build a Complete REST API (Serializer, Validation & Authentication)

Reagiere als Erste:r — dein Feedback zählt!

Learn how to build a modern REST API with Symfony 7 – from data validation with DTOs, to clean controllers, and best practices for maintainability.

In this example, we create a cocktails API 🍸

You’ll see how to:

✅ Use DTOs to validate incoming requests
✅ Map the request into an Entity with the ObjectMapper
✅ Keep controllers lean and easy to test

Create a cocktail (POST method)

When building modern APIs, using DTOs (Data Transfer Objects) and validation keeps your code clean, secure, and maintainable.

DTO with Validation

This DTO ensures that any request sent to your API follows the right rules (e.g. name length, valid URL, at least one ingredient).

<?php

namespace App\Dto;

use App\Entity\Cocktail;
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\Validator\Constraints as Assert;

#[Map(target: Cocktail::class)]
final readonly class CreateCocktailRequest
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Length(min: 2, max: 255)]
        public string $name,

        #[Assert\NotBlank]
        #[Assert\Length(min: 10)]
        public string $description,

        #[Assert\NotBlank]
        public string $instructions,

        #[Assert\NotBlank]
        #[Assert\Count(min: 1)]
        public array $ingredients,

        #[Assert\Range(min: 1, max: 5)]
        public int $difficulty,

        public bool $isAlcoholic,

        #[Assert\Url]
        public ?string $imageUrl,
    ) {
    }
}

Controller

Thanks to the ObjectMapper component, mapping between DTOs and entities is effortless.

<?php

namespace App\Controller\Api;

use App\Dto\CreateCocktailRequest;
use App\Entity\Cocktail;
use App\Repository\CocktailRepository;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
use Symfony\Component\Routing\Attribute\Route;

class CreateCocktailController
{
    public function __construct(
        private readonly CocktailRepository $cocktailRepository,
        private readonly ObjectMapperInterface $objectMapper
    ) {
    }

    #[Route('/api/cocktails', name: 'api.cocktails.create', methods: ['POST'])]
    public function __invoke(#[MapRequestPayload] CreateCocktailRequest $request): Response
    {
        $cocktail = $this->objectMapper->map($request, Cocktail::class);

        $this->cocktailRepository->save($cocktail);

        return new Response(null, Response::HTTP_CREATED);
    }
}

List all cocktails (GET method)

After creating cocktails, let’s build the endpoint to list them with filters.
Using Symfony 7, we can keep things clean and type-safe with a DTO, repository filters, and automatic query mapping.

Query DTO

This DTO makes query parameters explicit (name, isAlcoholic, difficulty, pagination…).

<?php

namespace App\Dto;

final readonly class ListCocktailsQuery
{
    public function __construct(
        public ?string $name = null,
        public ?bool $isAlcoholic = null,
        public ?int $difficulty = null,
        public int $page = 1,
        public int $itemsPerPage = 10,
    ) {
    }
}

Repository

Here, the repository handles filtering + pagination in a simple, reusable way.

    /**
     * @return Cocktail[]
     */
    public function findAllWithFilters(ListCocktailsQuery $query): array
    {
        $qb = $this->createQueryBuilder('cocktail');

        if ($query->name) {
            $qb
                ->andWhere('cocktail.name LIKE :name')
                ->setParameter('name', "%$query->name%");
        }

        if (null !== $query->isAlcoholic) {
            $qb
                ->andWhere('cocktail.isAlcoholic = :isAlcoholic')
                ->setParameter('isAlcoholic', $query->isAlcoholic);
        }

        if ($query->difficulty) {
            $qb
                ->andWhere('cocktail.difficulty = :difficulty')
                ->setParameter('difficulty', $query->difficulty);
        }

        $offset = ($query->page - 1) * $query->itemsPerPage;

        $qb
            ->setFirstResult($offset)
            ->setMaxResults($query->itemsPerPage);

        return $qb->getQuery()->getResult();
    }

Controller

Thanks to #[MapQueryString], Symfony 7 automatically maps query parameters into the DTO 💡.

<?php

namespace App\Controller\Api;

use App\Dto\ListCocktailsQuery;
use App\Repository\CocktailRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\SerializerInterface;

class ListCocktailsController
{
    public function __construct(
        private readonly CocktailRepository $cocktailRepository,
        private readonly SerializerInterface $serializer,
    ) {
    }

    #[Route('/api/cocktails', name: 'api.cocktails.list', methods: ['GET'])]
    public function __invoke(#[MapQueryString] ListCocktailsQuery $filter): Response
    {
        $cocktails = $this->cocktailRepository->findAllWithFilters($filter);

        $data = $this->serializer->serialize($cocktails, 'json', [
            'groups' => ['cocktail:read']
        ]);

        return JsonResponse::fromJsonString($data);
    }
}

This approach keeps your controllers slim, your queries explicit, and your API predictable.

Show one cocktail (GET)

After listing cocktails, let’s add the endpoint to fetch a single cocktail by its ID.

With Symfony 7, this becomes extremely clean thanks to #[MapEntity]:

Controller

<?php

namespace App\Controller\Api;

use App\Dto\ListCocktailsQuery;
use App\Entity\Cocktail;
use App\Repository\CocktailRepository;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\SerializerInterface;

class ShowCocktailController
{
    public function __construct(
        private readonly SerializerInterface $serializer,
    ) {
    }

    #[Route('/api/cocktails/{id}', name: 'api.cocktails.show', methods: ['GET'])]
    public function __invoke(#[MapEntity(expr: 'repository.find(id)', message: 'Not found')] Cocktail $cocktail,): Response
    {
        $data = $this->serializer->serialize($cocktail, 'json', [
            'groups' => ['cocktail:read']
        ]);

        return JsonResponse::fromJsonString($data);
    }
}

💡 With #[MapEntity], Symfony automatically fetches the Cocktail entity based on the {id} parameter.
If the entity does not exist, it returns a 404 Not Found automatically with your custom message.

Update a cocktail

Time to make our API editable.
With Symfony 7, we can accept partial updates (PATCH) and full replacements (PUT) using a dedicated Update DTO + the ObjectMapper to keep controllers slim and safe.

Update DTO

<?php

namespace App\Dto;

use App\Entity\Cocktail;
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\Validator\Constraints as Assert;

#[Map(target: Cocktail::class)]
final readonly class UpdateCocktailRequest
{
    public function __construct(
        #[Assert\Length(max: 255)]
        public ?string $name = null,

        #[Assert\Length(min: 10)]
        public ?string $description = null,

        #[Assert\NotBlank]
        public ?string $instructions = null,

        #[Assert\NotBlank]
        #[Assert\Count(min: 1)]
        public ?array $ingredients = null,

        #[Assert\Range(min: 1, max: 5)]
        public ?int $difficulty = null,

        public ?bool $isAlcoholic = null,

        #[Assert\Url]
        public ?string $imageUrl = null,
    ) {}
}

Controller

<?php

namespace App\Controller\Api;

use App\Dto\UpdateCocktailRequest;
use App\Entity\Cocktail;
use App\Repository\CocktailRepository;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\SerializerInterface;

class UpdateCocktailController
{
    public function __construct(
        private readonly CocktailRepository $cocktailRepository,
        private readonly ObjectMapperInterface $objectMapper,
        private readonly SerializerInterface $serializer
    ) {
    }

    #[Route('/api/cocktails/{id}', name: 'api.cocktails.update', methods: ['PUT', 'PATCH'])]
    public function __invoke(
        #[MapEntity(expr: 'repository.find(id)', message: 'Not found')] Cocktail $cocktail,
        #[MapRequestPayload] UpdateCocktailRequest $request
    ): JsonResponse {
        $updatedCocktail = $this->objectMapper->map($request, $cocktail);

        $this->cocktailRepository->save($updatedCocktail);

        $data = $this->serializer->serialize($updatedCocktail, 'json', [
            'groups' => ['cocktail:read']
        ]);

        return JsonResponse::fromJsonString($data);
    }
}

Delete a cocktail (DELETE method)

The last piece of our CRUD API: deleting a resource.
With Symfony 7, it stays clean and minimal thanks to #[MapEntity].

Controller

<?php

namespace App\Controller\Api;

use App\Entity\Cocktail;
use App\Repository\CocktailRepository;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class DeleteCocktailController
{
    public function __construct(
        private readonly CocktailRepository $cocktailRepository
    ) {
    }

    #[Route('/api/cocktails/{id}', name: 'api.cocktails.delete', methods: ['DELETE'])]
    public function __invoke( #[MapEntity(expr: 'repository.find(id)', message: 'Not found')] Cocktail $cocktail): Response
    {
        $this->cocktailRepository->remove($cocktail);

        return new Response(null, Response::HTTP_NO_CONTENT);
    }
}

API Key Authentication

Once our CRUD is ready, the next step is securing the API.
Here’s how to implement a custom API key authenticator using Symfony 7’s Security system.

Custom Authenticator

<?php

namespace App\Security;

use App\Entity\User;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

class ApiKeyAuthenticator extends AbstractAuthenticator
{
    public function supports(Request $request): ?bool
    {
        return $request->headers->has('API-KEY');
    }

    public function authenticate(Request $request): Passport
    {
        $apiKey = $request->headers->get('API-KEY');
        if ($apiKey !== 'secret') {
            throw new AuthenticationException('Invalid API key');
        }

        return new SelfValidatingPassport(
            new UserBadge($apiKey, fn() => (new User())->setUsername('API-USER'))
        );
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        $data = [
            'message' => strtr($exception->getMessageKey(), $exception->getMessageData()),
        ];

        return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
    }
}

security.yaml

    firewalls:
        main:
            custom_authenticators:
                - App\Security\ApiKeyAuthenticator
            lazy: true
            provider: app_user_provider
            stateless: true

Conclusion

And that’s it — we’ve just built a complete CRUD REST API with Symfony 7 🚀

  • Create a cocktail (POST)
  • Read cocktails (GET all / GET by id)
  • Update a cocktail (PUT/PATCH)
  • Delete a cocktail (DELETE)

Along the way, we used:

✅ DTOs for clean request validation
✅ ObjectMapper for mapping between DTOs and entities
✅ Serializer with groups for structured JSON responses
✅ MapEntity / MapRequestPayload / MapQueryString for cleaner controllers

This approach keeps controllers minimal, code maintainable, and APIs predictable.

👉 Full tutorial with extra steps (auth, filters, deployment on Cloudways): https://www.youtube.com/watch?v=Cd_9K749KfY

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Symfony 7: Build a Complete REST API (Serializer, Validation & Authentication)

Thematisch verwandte Begriffe: Symfony, Build, Complete, REST · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-93956 | A flaw has been found in olivier-ls PHP-FTS up to 1.1.2. Affected by thi…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick