nih / app-skeleton
Lightweight starter project for PSR-7/PSR-15 HTTP applications.
Requires
- php: 8.4 - 8.5
- nih/container: ^0.1.3
- nih/http-kernel: ^0.1.3
- nih/router: ^0.2.0
- psr/http-factory: ^1.1
- psr/http-message: ^2.0
- psr/log: ^3.0
Requires (Dev)
- phpunit/phpunit: ^11.5
This package is auto-updated.
Last update: 2026-04-11 22:10:31 UTC
README
Lightweight starter project for PSR-based HTTP applications built on top of nih/http-kernel.
It is intended for developers who want a ready-to-run application structure without committing to a large full-stack framework and without getting boxed into a tiny microframework.
If your first question is "why would I add this package at all?", start with docs/package-pains.md. It explains what problem each core NIH package is meant to solve and when it tends to become useful.
Contents
- Why this skeleton exists
- Package rationale: why each core package exists
- Requirements
- Quick start
- How a request flows
- First change in 2 minutes
- When routes start growing
- What you get out of the box
- What this skeleton intentionally does not include
- Core NIH packages
- What this stack makes easier
- Standards and ecosystem reuse
- Application model
- When this skeleton is a good fit
- When it is not a good fit
- Optional external integrations
- Development
Why this skeleton exists
PSR-7, PSR-15, PSR-17, and PSR-11 are extremely useful because they created a broad ecosystem of reusable HTTP messages, middleware, handlers, factories, and containers.
What those standards do not define is the shape of a complete HTTP application. In practice that often leaves two common choices:
- large frameworks that provide everything, but also bring a lot of structure, conventions, and moving parts
- microframeworks that start fast, but become awkward once the application needs richer routing, explicit composition, multiple pipelines, or host-aware rules
nih/app-skeleton targets the middle ground:
- fast start for a small HTTP application
- ability to start writing business logic immediately instead of first assembling framework plumbing
- explicit and lightweight project structure
- PSR-compatible HTTP stack from day one
- room to grow into more advanced routing and middleware composition
If you want the package-by-package view of which concrete pain each part of the stack is solving, see docs/package-pains.md.
Requirements
- PHP
8.4or8.5 - Composer
Quick start
For a new application, use Composer project creation:
composer create-project nih/app-skeleton my-app cd my-app composer test composer serve
Then open:
http://127.0.0.1:8080/http://127.0.0.1:8080/health
The goal of the skeleton is that after installation you can move straight to application behavior: add routes, handlers, middleware, and domain code without first building a custom public entry point, container wiring strategy, or bootstrap lifecycle from scratch.
If you are working on the skeleton itself rather than creating a new app from it:
git clone https://github.com/nih-soft/app-skeleton.git cd app-skeleton composer install composer test
How a request flows
The default mental model is deliberately small:
public/index.phploads Composer and reads the ordered bootstrap list fromconfig/app.php.- Bootstrap classes configure services, routes, the main pipeline, and the error pipeline.
- The default main pipeline is
RouteMatchMiddleware->RouteDispatchMiddleware, withNotFoundHandleras the fallback final handler. - A matched route resolves the action class from the container and executes it.
- Unhandled exceptions go through the separate error pipeline:
ErrorLoggingMiddleware->ErrorFormatMiddleware, withPlainTextErrorHandleras the fallback final handler.
That is enough to understand where to make most first changes: routes and services live in bootstraps, request behavior lives in middleware and actions.
First change in 2 minutes
Once the app is running, the fastest way to touch real behavior is:
- edit
src/Action/IndexAction.phpto change the response of/ - edit
src/Bootstrap/AppBootstrap.phpto add routes or global middleware - create more bootstrap classes when configuration becomes too large for one file
Typical example:
$app->routes->path('/about') ->action('', App\Action\AboutAction::class, '__invoke', ['GET']); $app->pipeline->prepend(App\Http\Middleware\RequestIdMiddleware::class);
What this means in practice:
path('/about')creates the/aboutbranch in the route treeaction('', ...)attaches the action to that exact branch root rather than to a child path segmentApp\Action\AboutAction::classandApp\Http\Middleware\RequestIdMiddleware::classare class strings; the container resolves them when the request is dispatchedprepend()makes the middleware run before routing, which is the usual choice for request-wide behavior such as request IDs, auth context, or body parsing
If you use plain append() in AppBootstrap, that middleware is added after RouteDispatchMiddleware and usually acts as fallback behavior for NOT_FOUND pass-through requests rather than for successfully dispatched routes.
Bootstrap classes define application configuration. Actions and middleware handle requests.
When routes start growing
If a whole route subtree follows naming conventions, one branch rule can cover many actions without listing them one by one:
use NIH\Router\Strategy\PathToClassStrategy; $app->routes->path('/forums/') ->strategy(PathToClassStrategy::class, [ 'namespace' => 'App\\Action\\Forums', ]);
Typical mappings for that subtree:
GET /forums/->App\Action\Forums\GetGET /forums/view->App\Action\Forums\ViewGet
What you get out of the box
- a thin public entry point in
public/index.php - ordered bootstraps in
config/app.php - app-specific wiring in
src/Bootstrap/ - demo actions in
src/Action/ - a default main pipeline:
RouteMatchMiddleware->RouteDispatchMiddleware->NotFoundHandler - explicit error pipeline wiring with content-negotiated error rendering and logging
- host-agnostic starter routing by default
- a lightweight PSR-based stack that can keep growing with the application
Those files are examples of a minimal layout, not mandatory framework rules. You may keep everything in public/index.php, move bootstrap lists elsewhere, or organize the project in any other way that fits the application.
What this skeleton intentionally does not include
This is a minimal skeleton, not a batteries-included framework distribution.
Out of the box it does not include:
- a production logging stack beyond the minimal app-local logger used for error logging examples
- an ORM
- database access or migrations
- higher-level framework subsystems such as auth, forms, templating, or admin tooling
You are expected to add the libraries that fit your application and keep the project structure that fits your team. The skeleton is intentionally open in that regard.
Core NIH packages
If your first question is "why would I add this package and what pain does it solve?", start with docs/package-pains.md.
If you already know why you need a package and want the package-level API and runtime documentation, start with the README of each core package:
nih/http-kernel: minimal application kernel and bootstrap lifecycle for PSR-7, PSR-11, PSR-15, and PSR-17 based applicationsnih/container: PSR-11 container with autowiring for explicit application compositionnih/router: tree-based router with runtime-built configuration, site-aware matching, URL generation, and PSR-15 middleware integrationnih/middleware-dispatcher: middleware dispatcher designed for lazy resolution and large or dynamic PSR-15 pipelines
nih/http-kernel is the top-level runtime package. The skeleton also declares nih/container and nih/router directly because application code and bootstrap configuration use their APIs explicitly. nih/middleware-dispatcher remains a transitive dependency pulled in by the kernel and router packages.
What this stack makes easier
- keeping boot-time configuration explicit in PHP instead of spreading it across hidden framework conventions
- separating normal request flow from error logging and negotiated error rendering
- starting with a few explicit routes and growing into larger route trees without changing routing model
- placing middleware before routing, on route branches, or in fallback positions instead of forcing everything into one flat global stack
- resolving actions and middleware lazily from the container through class strings
- growing the application without adding a build step for container compilation or route compilation
- reusing PSR-compatible middleware, handlers, loggers, and supporting packages without wrapping everything in framework-specific HTTP abstractions
- integrating new subsystems gradually instead of committing to a batteries-included framework from day one
Standards and ecosystem reuse
The skeleton is built around standard PSR contracts instead of framework-specific HTTP abstractions:
psr/http-messagefor PSR-7 request and response objectspsr/http-factoryfor PSR-17 factories- PSR-15 request handlers and middleware through the HTTP stack
psr/logfor logging contracts
That matters because it lets the application reuse a wide range of existing PSR-compatible middleware, handlers, and supporting packages instead of forcing everything through a custom framework API.
Application model
README stays focused on onboarding. The detailed application model lives in docs/application-model.md.
That document covers:
- project structure and the role of
public/index.php,config/app.php, bootstrap classes, actions, and tests - startup flow and bootstrap execution order
- the bootstrap contract and code-based configuration model
- the default main pipeline and error pipeline
- current demo behavior, including starter routes and host-agnostic matching defaults
When this skeleton is a good fit
- small or medium HTTP applications that should stay explicit and lightweight
- projects where developers want to start with business logic quickly instead of spending early time on framework assembly
- projects that want to reuse existing PSR middleware and handlers
- teams that want a clear startup model instead of hidden framework magic
- applications that may outgrow a microframework but do not need a full-stack framework
When it is not a good fit
- applications that want a batteries-included full-stack framework from day one
- teams that want the framework itself to prescribe most application architecture and built-in subsystems
- projects that expect built-in ORM, forms, templating, auth, admin tools, and similar higher-level features in the base package
Optional external integrations
The current starter model does not include a built-in local modules/ system. Optional integrations should be separate packages or other explicitly listed bootstrap classes.
For example, if an application needs to fall through to an existing legacy entry point when the new routes do not match, it can add nih/legacy-gateway explicitly and include NIH\LegacyGateway\LegacyGatewayBootstrap::class in config/app.php.
Development
composer test