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 $logErrors on 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() and return $response from every route handler.
  • Never call echo or print directly 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/201 for success, 4xx for client mistakes, 5xx for 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.
Quick recap:
PracticeCovered in
Hide error details in productionError Handling
Organize routes into groupsRoute Groups
Share services via the DI containerDependency Injection
Return the response, never echoRouting Basics
Validate and sanitize inputForm Validation
Use HTTP status codes consistentlyBuilding JSON APIs
Keep closures thinThis page
You've completed the Slim Framework track! Congratulations on working through everything from your first route to templates, databases, JSON APIs, and error handling. The concepts underneath — PSR-7 requests and responses, routing, and middleware — aren't unique to Slim; bigger frameworks like Laravel and Symfony (coming soon) use the very same ideas, they just automate more of the wiring for you. Click Next below to head back to the PHP hub and pick another track to continue learning.