Best Practices
This page is a capstone checklist for the whole Slim Framework track — the habits worth carrying into every real project. Each item links back to the page that covers it in full, so use this as a quick-reference summary rather than a first read.
1) Keep $displayErrorDetails Off in Production
Slim's error middleware can print full stack traces straight to the browser, which is great while you're developing and dangerous once real visitors can see it. See Error Handling for the full picture.
<?php
$displayErrorDetails = ($_ENV['APP_ENV'] ?? 'production') === 'development';
$app->addErrorMiddleware($displayErrorDetails, true, true);
- Drive the flag from an environment variable so the same code deploys safely everywhere.
- Always keep
$logErrorson so you still have a record even when the detail is hidden.
2) Use Route Groups to Organize Related Endpoints
Once an app has more than a handful of routes, listing them all flat gets hard to scan. Group routes that share a prefix or middleware together. See Route Groups.
<?php
$app->group('/users', function ($group) {
$group->get('', ListUsersAction::class);
$group->get('/{id}', ShowUserAction::class);
});
- Group routes by resource (
/users,/posts) rather than leaving them flat and scattered. - Attach shared middleware (auth, rate limiting) to the group instead of repeating it on every route.
3) Let the DI Container Manage Shared Services
A database connection, a logger, a mailer — anything shared across routes belongs in the container, built once and reused, instead of being instantiated fresh inside every closure. See Dependency Injection.
<?php
$container->set('db', function () {
return new PDO('mysql:host=localhost;dbname=app', 'user', 'pass');
});
- Register shared services once in the container, not with
new PDO(...)scattered through route handlers. - Pull dependencies out of the container in your action classes instead of reaching for globals.
4) Always Return the Response, Never Echo
Slim builds its output from the PSR-7 response object your handler returns. Echoing text directly bypasses that pipeline and can break headers, middleware, and content length. See Routing Basics.
<?php
// Good
$app->get('/hello', function ($request, $response) {
$response->getBody()->write('Hello!');
return $response;
});
- Write to
$response->getBody()andreturn $responsefrom every route handler. - Never call
echoorprintdirectly inside a route closure or controller action.
5) Validate and Sanitize Every Incoming Request
Route parameters, query strings, and POST bodies are all attacker-controlled input until proven otherwise. The same validation discipline from Core PHP's Form Validation page applies here — Slim doesn't validate anything for you.
<?php
$data = (array) $request->getParsedBody();
$email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
if ($email === false) {
return $response->withStatus(422);
}
- Never trust route parameters, query strings, or request bodies without checking them first.
- Reject invalid input early with a clear status code rather than letting it flow into your business logic.
6) Use Proper HTTP Status Codes Consistently
An API that always returns 200, even on failure, forces every caller to parse the response body
just to know what happened. Set the status that matches the outcome. See Building JSON APIs.
<?php
if (!$user) {
return $response->withStatus(404);
}
return $response->withStatus(201); // created
- Use
200/201for success,4xxfor client mistakes,5xxfor server failures. - Keep status codes consistent across every endpoint so API consumers can rely on them.
7) Keep Route Closures Thin
A route closure is a good place to start, but as an app grows, cramming validation, database queries, and response formatting into one anonymous function makes it impossible to test or reuse. Move the real work into a separate class or service, and let the route just wire things together.
<?php
// Thin: the closure only delegates
$app->post('/users', function ($request, $response) {
$service = new UserRegistrationService($this->get('db'));
$user = $service->register((array) $request->getParsedBody());
$response->getBody()->write(json_encode($user));
return $response->withStatus(201);
});
- Push validation, database access, and business rules into plain classes the closure just calls.
- A thin closure is easy to read at a glance and easy to unit test the logic behind it separately.
| Practice | Covered in |
|---|---|
| Hide error details in production | Error Handling |
| Organize routes into groups | Route Groups |
| Share services via the DI container | Dependency Injection |
| Return the response, never echo | Routing Basics |
| Validate and sanitize input | Form Validation |
| Use HTTP status codes consistently | Building JSON APIs |
| Keep closures thin | This page |