Skip to content

About

Empty Perfect Starter Project.

Topics

Resources

Stars

221 stars

Watchers

19 watching

Forks

Repository files navigation

PerfectTemplate

A reference application for the Perfect Swift 6 libraries. It's a small blog-style "Posts" CRUD web app that wires together Perfect-NIO, Perfect-CRUD, Perfect-MySQL, and Perfect-Logger: a JSON API, MySQL persistence, structured logging, and a static browser UI. Copy it as the starting point for a new app built on them.

The pre-Swift-6 version of this package is preserved on the legacy branch.

Requirements

  • Swift 6.2 or later (swift-tools-version 6.2)
  • macOS 12 or later, or Linux (CI builds on Ubuntu 24.04)
  • The MySQL client library and pkg-config:
    • macOS: brew install mysql-client pkgconf, then export PKG_CONFIG_PATH="$(brew --prefix mysql-client)/lib/pkgconfig" (the formula is keg-only)
    • Debian/Ubuntu: apt-get install libmysqlclient-dev pkg-config
  • A MySQL 5.7.8+ or MariaDB 10.1+ server (tested with MySQL 8.4 and MariaDB 11.8). The quick start runs MySQL in a container; mariadb:11.8 works the same way with MARIADB_* variables in place of MYSQL_*.

On macOS the linker may warn that Homebrew's libmysqlclient was built for a newer macOS than the package's macOS 12 deployment target. The app still builds and runs on the machine it was built on.

Dependencies

Package Version Products used
Perfect-NIO 0.1.0 PerfectNIO
Perfect-CRUD 3.1.0 PerfectCRUD
Perfect-MySQL 4.0.0 PerfectMySQL
Perfect-Logger 4.0.0 PerfectLogger
swift-nio 2.65.0 NIOPosix (already used by Perfect-NIO)

Versions are minimums (from:), so swift package update picks up compatible releases.

Quick start

Start MySQL in a container. It is published on host port 13306, not 3306, so it can't collide with a MySQL server you already run; the app's defaults point at it.

docker run -d --name perfect-template-mysql -p 13306:3306 \
    -e MYSQL_ROOT_PASSWORD=change-me \
    -e MYSQL_DATABASE=perfect_template \
    -e MYSQL_USER=perfect -e MYSQL_PASSWORD=perfect \
    mysql:8.4

(With Apple's container tool, the same command works with container run.) Give MySQL a few seconds to initialise, then build and run the app from the package root:

swift run

Open http://localhost:9191 for the overview page and http://localhost:9191/posts for the browser UI, or use the API:

curl http://localhost:9191/api/health
curl -X POST http://localhost:9191/api/posts \
     -H "Content-Type: application/json" \
     -d '{"title":"Hello","body":"World"}'
curl http://localhost:9191/api/posts

The Post table is created on the first successful connection. If MySQL isn't reachable the server starts anyway: database routes answer 503 until it is. The app remembers that the table exists, so if you drop it while the app runs, restart the app.

Configuration

Everything comes from environment variables (see Sources/App/Config.swift). The database defaults match the quick start container above.

Variable Default Purpose
PORT 9191 HTTP port, 1–65535 (the server listens on all interfaces)
DB_HOST 127.0.0.1 MySQL host
DB_PORT 13306 MySQL port, 1–65535. Set 3306 for a server on the standard port
DB_NAME perfect_template MySQL database; it must exist
DB_USER perfect MySQL user
DB_PASS perfect MySQL password
WEBROOT webroot Directory with the HTML pages and static/, relative to the working directory
LOG_FILE ./perfect-template.log File the log is appended to
LOG_LEVEL info debug, info, warning, error or critical. debug also logs the SQL PerfectCRUD runs

A PORT or DB_PORT that isn't a whole number from 1 to 65535 stops the app at launch with a message saying so.

For example, against an existing server:

DB_HOST=db.internal DB_PORT=3306 DB_USER=app DB_PASS=secret swift run

Run from the package root (or set WEBROOT to an absolute path), since the pages are read from disk at request time. In Xcode, set the scheme's working directory to the package folder.

Routes

Method Path Description
GET, HEAD / Overview page (webroot/index.html)
GET, HEAD /posts Browser UI for the API (webroot/posts.html)
GET, HEAD /static/** Files under webroot/static/
GET /api/health Health check; doesn't touch the database
GET /api/posts All posts, newest first
POST /api/posts Create a post: {"title": "…", "body": "…"} (body optional)
GET /api/posts/:id One post
PUT /api/posts/:id Update title, body, or both
DELETE /api/posts/:id Delete a post

POST and PUT take a JSON body sent with Content-Type: application/json. A PUT writes only the fields it sends, so two clients updating different fields don't undo each other.

A post looks like this; id is a UUID and createdAt an ISO 8601 time in UTC:

{"id":"9C3A0D2E-0B7C-4C1E-9E55-2F1F7A0B6E1D","title":"Hello","body":"World","createdAt":"2026-10-15T16:10:51Z"}

Errors are plain text with the status code saying what went wrong:

Status When
400 The JSON doesn't fit the route (for example, no title on create); the title is empty or only invisible characters, over 200 characters or 2,000 Unicode code points, or contains a control character, line or paragraph separator, or bidirectional control; or the body is over 20,000 characters or 100,000 code points. Also any request path with a .. segment
404 No post with that id (or the id isn't a UUID), or no such page or file
413 The request body is over 256 KiB
415 POST or PUT without Content-Type: application/json
503 "Database unavailable.": no connection to MySQL could be opened within 5 seconds, or one failed in the last 2 seconds. "Database busy.": the server gave up on the statement after waiting 20 seconds for a lock, or after it ran 20 seconds (on MySQL only a SELECT is limited; on MariaDB every statement is). Nothing was written, since each request's statements autocommit; code that adds explicit transactions has to roll back itself
500 Anything else, such as a failed query or no answer from MySQL within 30 seconds; the details go to the log, not the client

How it's put together

  • main.swift checks the configuration, bootstraps PerfectLogger (console and file), sends PerfectCRUD's own logging to it at debug level, connects to MySQL in the background so the schema exists, and starts the Perfect-NIO Server with a 256 KiB request body limit.
  • DB.swift has withDB {}, which opens a MySQL connection, makes sure the table exists, and runs the work on a pool of 16 threads used only for the database (a swift-nio NIOThreadPool). Blocking libmysqlclient calls stay off Swift's cooperative thread pool, and when MySQL is slow or a table is locked, at most 16 requests wait on it while the health check, pages and other requests keep answering. Connecting times out after 5 seconds, and after a failed attempt requests answer 503 at once for 2 seconds rather than each waiting again. Each connection tells the server to give up on a statement that waits over 20 seconds for a lock, or runs over 20 seconds (MySQL's max_execution_time limits only SELECTs; MariaDB's max_statement_time limits every statement), before the client's own 30-second read timeout: a request that fails that way has not written anything, and the server isn't left working on a session nobody is waiting for. It opens a connection per request; for a busy service, look at PerfectCRUD's DatabaseConnectionPool.
  • Routes/APIRoutes.swift is the JSON API, with input validation and error mapping.
  • Routes/WebRoutes.swift serves the pages and static files. Perfect-NIO refuses any request path with a .. segment before routing, so static paths can't leave webroot/static.
  • Models/Post.swift is the PerfectCRUD model and request types.

Checking a running server

Scripts/smoke-test.sh drives every API route against a running server (create, list, get, update, delete, and the error cases). Scripts/smoke-test.sh --no-db checks a server that has no database: health and pages answer 200 and /api/posts answers 503. CI runs both.

License

Apache 2.0; see LICENSE (中文).

About

Empty Perfect Starter Project.

Topics

Resources

Stars

221 stars

Watchers

19 watching

Forks

Releases

Packages

Contributors

Languages