How to migrate to API Platform
Edit on GitHubIf you’re migrating multiple modules in one go (the default), follow the API Platform migration overview first — it covers the shop-baseline upgrade, project-config checklist, and batch cleanup. This document is the per-module deep dive referenced from that overview.
This document describes how to migrate existing Glue API resources to the API-Platform while maintaining backward compatibility.
Overview
Migrating from Glue API to API Platform provides several benefits:
- Schema-based development: Define resources declaratively in YAML instead of PHP code
- Automatic OpenAPI documentation: Interactive API docs generated from schemas
- Reduced boilerplate: No need for manual resource builders, mappers, and route definitions
- Built-in validation: Declarative validation rules with operation-specific constraints
- Standardized pagination: Consistent pagination across all resources
- Better maintainability: Clearer separation of concerns with providers and processors
The recommended default is batch migration — migrating a group of related modules together, as described in the API Platform migration overview. The per-resource steps below are the mechanics you apply to each resource within a batch; none of it breaks existing API consumers.
Prerequisites
Before migrating resources, ensure you have:
- Integrated API Platform as described in How to integrate API Platform
- Configured router plugins in correct order (see below)
- Tested that API Platform is working with at least one test resource
Migration strategy and router setup
This guide covers the mechanics of migrating a single resource. The overall strategy (batch migration is the default), the router-plugin ordering, and how routing flips between Glue and API Platform are owned by the API Platform migration overview — read it first. The steps below are what you apply to each resource within a batch.
Migration process
Step 1: Identify resources to migrate
List all existing Glue resources in your application:
Backend API resources are typically registered in:
\Pyz\Glue\GlueBackendApiApplication\GlueBackendApiApplicationDependencyProvider::getResourcePlugins()
Storefront API resources are typically registered in:
\Pyz\Glue\GlueApplication\GlueApplicationDependencyProvider::getResourceRoutePlugins()
Create a migration checklist:
[ ] Customers resource
[ ] Products resource
[ ] Orders resource
[ ] Cart resource
[ ] Wishlist resource
...
Start with simpler, read-only resources (GET operations only) before migrating complex resources with write operations and business logic.
Step 2: Analyze existing Glue resource
Before migrating, understand the existing resource structure.
Example: Existing Glue Customer Resource
-
Resource route plugin:
src/Pyz/Glue/CustomersRestApi/Plugin/GlueApplication/CustomersResourceRoutePlugin.php -
Resource class:
src/Pyz/Glue/CustomersRestApi/Processor/Customer/CustomerReader.php -
Attributes transfer:
src/Generated/Shared/Transfer/RestCustomersAttributesTransfer.php -
Operations supported:
- GET
/customers/{customerReference}- Get single customer - GET
/customers- Get customer collection - POST
/customers- Create customer - PATCH
/customers/{customerReference}- Update customer
- GET
Step 3: Create API Platform schema
Create the equivalent API Platform schema for the resource.
Map Glue concepts to API Platform:
| Glue API | API Platform |
|---|---|
| Resource class | Provider class |
| Resource builder | Schema definition (YAML) |
| Attributes transfer | Resource class (auto-generated) |
| Reader | Provider |
| Writer | Processor |
| Resource route plugin | Operations in schema |
| Relationship plugins | Properties in schema |
Create schema file:
src/Pyz/Zed/Customer/resources/api/backend/customers.yml
resource:
name: Customers
shortName: Customer
description: "Customer resource for backend API"
provider: "Pyz\\Glue\\Customer\\Api\\Backend\\Provider\\CustomerBackendProvider"
processor: "Pyz\\Glue\\Customer\\Api\\Backend\\Processor\\CustomerBackendProcessor"
paginationEnabled: true
paginationItemsPerPage: 10
operations:
- type: Post
- type: Get
- type: GetCollection
- type: Patch
properties:
customerReference:
type: string
description: "A unique reference for a customer."
writable: false
identifier: true
email:
type: string
description: "The email address of the customer."
openapiContext:
example: "[email protected]"
firstName:
type: string
description: "The first name of the customer."
openapiContext:
example: "John"
lastName:
type: string
description: "The last name of the customer."
openapiContext:
example: "Doe"
# Map all properties from RestCustomersAttributesTransfer
Create validation schema:
src/Pyz/Zed/Customer/resources/api/backend/customers.validation.yml
post:
email:
- NotBlank:
message: "Email is required"
- Email:
message: "Invalid email format"
firstName:
- NotBlank:
message: "First name is required"
lastName:
- NotBlank:
message: "Last name is required"
patch:
email:
- Optional:
constraints:
- Email
Step 4: Implement Provider
Create the Provider to handle read operations, reusing existing business logic.
The Provider should primarily call existing Facade methods. This ensures consistency and reduces duplication of business logic.
src/Pyz/Zed/Customer/Api/Backend/Provider/CustomerBackendProvider.php
<?php
namespace Pyz\Zed\Customer\Api\Backend\Provider;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\Pagination\TraversablePaginator;
use ApiPlatform\State\ProviderInterface;
use Generated\Api\Backend\CustomersBackendResource;
use Spryker\Zed\Customer\Business\CustomerFacadeInterface;
class CustomerBackendProvider implements ProviderInterface
{
public function __construct(
private CustomerFacadeInterface $customerFacade,
) {
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
if (isset($uriVariables['customerReference'])) {
return $this->getCustomer($uriVariables['customerReference']);
}
return $this->getCustomers($context);
}
private function getCustomer(string $customerReference): ?CustomersBackendResource
{
// Reuse existing Glue logic
$customerTransfer = $this->customerFacade->findCustomerByReference($customerReference);
if ($customerTransfer === null) {
return null;
}
// Map transfer to API Platform resource
$resource = new CustomersBackendResource();
$resource->fromArray($customerTransfer->toArray());
return $resource;
}
private function getCustomers(array $context): TraversablePaginator
{
$filters = $context['filters'] ?? [];
$page = (int) ($filters['page'] ?? 1);
$itemsPerPage = (int) ($filters['itemsPerPage'] ?? 10);
// Reuse existing facade method
$customerCollection = $this->customerFacade->getCustomerCollection($page, $itemsPerPage);
$resources = [];
foreach ($customerCollection->getCustomers() as $customerTransfer) {
$resource = new CustomersBackendResource();
$resource->fromArray($customerTransfer->toArray());
$resources[] = $resource;
}
return new TraversablePaginator(
new \ArrayObject($resources),
$page,
$itemsPerPage,
$customerCollection->getTotalCount()
);
}
}
Step 5: Implement Processor
Create the Processor to handle write operations.
The Processor should primarily call existing Facade methods. This ensures consistency and reduces duplication of business logic.
src/Pyz/Zed/Customer/Api/Backend/Processor/CustomerBackendProcessor.php
<?php
namespace Pyz\Zed\Customer\Api\Backend\Processor;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\State\ProcessorInterface;
use Generated\Api\Backend\CustomersBackendResource;
use Generated\Shared\Transfer\CustomerTransfer;
use Spryker\Zed\Customer\Business\CustomerFacadeInterface;
class CustomerBackendProcessor implements ProcessorInterface
{
public function __construct(
private CustomerFacadeInterface $customerFacade,
) {
}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
if ($operation instanceof Post) {
return $this->createCustomer($data);
}
if ($operation instanceof Patch) {
return $this->updateCustomer($data, $uriVariables['customerReference']);
}
return null;
}
private function createCustomer(CustomersBackendResource $resource): CustomersBackendResource
{
$customerTransfer = new CustomerTransfer();
$customerTransfer->fromArray($resource->toArray(), true);
// Reuse existing facade method
$customerResponseTransfer = $this->customerFacade->addCustomer($customerTransfer);
$result = new CustomersBackendResource();
$result->fromArray($customerResponseTransfer->getCustomerTransfer()->toArray());
return $result;
}
private function updateCustomer(CustomersBackendResource $resource, string $customerReference): CustomersBackendResource
{
$customerTransfer = new CustomerTransfer();
$customerTransfer->fromArray($resource->toArray(), true);
$customerTransfer->setCustomerReference($customerReference);
// Reuse existing facade method
$customerResponseTransfer = $this->customerFacade->updateCustomer($customerTransfer);
$result = new CustomersBackendResource();
$result->fromArray($customerResponseTransfer->getCustomerTransfer()->toArray());
return $result;
}
}
Step 6: Generate API Platform resource
Generate the backend resource class from the schema:
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate
# Verify generation
ls -la src/Generated/Api/Backend/CustomersBackendResource.php
Step 7: Test the API Platform endpoint
Test that the new endpoint works correctly:
# Test single resource
curl -X GET http://glue-backend.eu.spryker.local/customers/DE--1
# Test collection
curl -X GET http://glue-backend.eu.spryker.local/customers?page=1&itemsPerPage=10
# Test create
curl -X POST http://glue-backend.eu.spryker.local/customers \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","firstName":"John","lastName":"Doe"}'
# Test update
curl -X PATCH http://glue-backend.eu.spryker.local/customers/DE--1 \
-H "Content-Type: application/json" \
-d '{"firstName":"Jane"}'
Verify:
- ✅ Responses match expected format
- ✅ Validation rules work correctly
- ✅ Error handling is appropriate
- ✅ Pagination works for collections
- ✅ OpenAPI documentation is generated at root URL
/
Step 8: Run existing Glue API tests
Ensure backward compatibility by running existing tests:
# Run Glue API tests
vendor/bin/codecept run -c tests/PyzTest/Glue/CustomersRestApi
# Or specific test
vendor/bin/codecept run -c tests/PyzTest/Glue/CustomersRestApi/RestApi/CustomerRestApiCest
All existing tests should still pass because:
GlueRouterPluginis checked first- Existing Glue endpoints still work
- No breaking changes to consumers
Step 9: Remove Glue resource files
The actual switch from Glue REST to API Platform for this module is removing its *ResourceRoutePlugin from the project-level dependency provider (shown below). The optional excludedPathFragments setting in spryker_api_platform.php controls schema generation only — it does not flip routing. The spryker/<module>-rest-api composer package may stay installed; it simply no longer serves routes once the plugin is unregistered.
Once the API Platform endpoint is working and tested, remove the old Glue files:
# Remove resource route plugin
rm src/Pyz/Glue/CustomersRestApi/Plugin/GlueApplication/CustomersResourceRoutePlugin.php
# Remove processor classes
rm -rf src/Pyz/Glue/CustomersRestApi/Processor/
# Update dependency provider to remove plugin registration
Update GlueApplicationDependencyProvider:
src/Pyz/Glue/GlueApplication/GlueApplicationDependencyProvider.php
protected function getResourceRoutePlugins(): array
{
return [
// new CustomersResourceRoutePlugin(), // ← Remove this line
new ProductsResourceRoutePlugin(),
new OrdersResourceRoutePlugin(),
// ... keep other plugins
];
}
Step 10: Verify migration
After removing Glue resource files:
# Clear caches
console cache:clear
# Test that API Platform endpoint still works
curl -X GET http://glue-backend.eu.spryker.local/customers/DE--1
# Verify OpenAPI docs include the resource
curl http://glue-backend.eu.spryker.local/docs.json | jq '.paths'
# Check the interactive documentation at root URL
# Visit: http://glue-backend.eu.spryker.local/
Step 11: Repeat for remaining resources
Repeat steps 2-10 for each resource in your migration checklist:
[✓] Customers resource ← Migrated
[ ] Products resource ← Next
[ ] Orders resource
[ ] Cart resource
[ ] Wishlist resource
...
Migration comparison
Before: Glue API
Request: GET /customers/DE--1
↓
GlueRouterPlugin
↓
CustomersResourceRoutePlugin
↓
CustomerReaderInterface
↓
CustomerFacade
↓
RestResourceBuilder
↓
Response: RestCustomersAttributesTransfer
After: API Platform
Request: GET /customers/DE--1
↓
SymfonyFrameworkRouterPlugin
↓
API Platform Router
↓
CustomerBackendProvider
↓
CustomerFacade (same!)
↓
CustomersBackendResource
↓
Response: JSON (auto-serialized)
Key differences
| Aspect | Glue API | API Platform |
|---|---|---|
| Definition | PHP classes & plugins | YAML schemas |
| Routing | ResourceRoutePlugin | Schema operations |
| Reading data | Reader classes | Provider classes |
| Writing data | Writer classes | Processor classes |
| Validation | Manual in reader/writer | Declarative in validation schema |
| Documentation | Separate OpenAPI schema | Auto-generated from schema |
| Response building | Manual RestResourceBuilder | Auto-serialization |
| Relationships | Relationship plugins | Schema properties |
| File count | ~10-15 files per resource | ~3-5 files per resource |
Troubleshooting migration
Both old and new endpoints respond
Symptom: Both Glue and API Platform endpoints return responses.
Cause: Different URLs are being used. Check if they’re actually the same:
# Glue endpoint
GET /customers/DE--1
# API Platform endpoint
GET /customers/DE--1
# Check URL prefixes in configuration
Solution: Ensure URLs match exactly. API Platform resources use shortName for URL generation.
API Platform endpoint returns 404 during migration
Symptom: After creating schema and generating resource, endpoint returns 404.
Possible causes:
- Router order is wrong (SymfonyFrameworkRouterPlugin before GlueRouterPlugin)
- Cache not cleared
- Resource not generated
Solution:
# Check router order in RouterDependencyProvider
# Should be: GlueRouterPlugin, then SymfonyFrameworkRouterPlugin
# Clear caches
console cache:clear
# Regenerate resources
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate
# Verify generated file exists
ls -la src/Generated/Api/Backend/CustomersBackendResource.php
Different response format between Glue and API Platform
Symptom: API Platform returns different JSON structure than Glue.
Cause: Glue uses JSON:API format, API Platform uses JSON-LD by default which is configurable and depending on your needs you can migrate to JSON-LD as well or stay with the JSON API format. API-Platform covers this possibility for you
Solution:
This is expected. You have three options:
- Accept the difference (recommended): Update API consumers to handle both formats during migration
- Configure API Platform format: Customize serialization to match the Glue format. See Serialization for how API Platform serialization works and how to register custom normalizers.
- Use content negotiation: Support both formats based on
Acceptheader
Business logic differs between implementations
Symptom: API Platform endpoint behaves differently than a Glue endpoint.
Cause: Provider/Processor uses different facade methods or has different logic.
Solution:
Review and ensure both use the same facade methods:
// Glue Reader
$customerReader->readCustomer($customerReference);
↓ calls
$this->customerFacade->findCustomerByReference($customerReference);
// API Platform Provider
$this->customerFacade->findCustomerByReference($customerReference); // ← Same method!
Best practices
1. Keep batches small
Batch migration is the default (see the migration overview), but keep each batch small and ship it before starting the next — don’t try to migrate every resource in one go. For example:
Sprint 1: Customers, Products (read-only)
Sprint 2: Orders, Cart
Sprint 3: Wishlist, Checkout
2. Keep business logic in facades
Don’t duplicate business logic in Providers/Processors:
// ❌ Bad: Logic in Provider
private function getCustomer(string $reference): ?CustomersBackendResource
{
$customer = $this->repository->findByReference($reference);
// ... business logic here
}
// ✅ Good: Delegate to Facade
private function getCustomer(string $reference): ?CustomersBackendResource
{
$customerTransfer = $this->customerFacade->findCustomerByReference($reference);
return $this->mapToResource($customerTransfer);
}
3. Use toArray/fromArray for mapping
Leverage generated toArray() and fromArray() methods:
// Easy mapping between Transfer and Resource
$resource = new CustomersBackendResource();
$resource->fromArray($customerTransfer->toArray());
4. Test thoroughly before removing Glue code
- Run all existing tests
- Perform manual testing
- Check with API consumers
- Monitor production traffic
5. Document breaking changes
If response formats differ, document changes for API consumers:
## Migration Notice: Customers API
The `/customers` endpoint is being migrated to API-Platform.
### Changes:
- Response format: JSON:API → JSON-LD
- Date format: unix timestamp → ISO 8601
- Error format: JSON:API errors → RFC 7807 Problem Details
### Timeline:
- Old endpoint: Supported until 2026-12-31
- New endpoint: Available now
- Deprecation: Old endpoint will return deprecation headers starting 2026-09-01
Next steps
- API Platform - Architecture overview
- API Platform Enablement - Creating resources
- Resource Schemas - Resource Schemas
- Validation Schemas - Validation Schemas
- Troubleshooting - Common issues
Thank you!
For submitting the form