The current Internet ecosystem has been completely changed by APIs, and there is good reason. By using third-party APIs in your product or service, you can access a wide range of useful features—such as authentication or storage services—which are beneficial to you and your users. By exposing your own API, your application will become "part of the composition" and use it in a way you never thought... Of course, if you do this the right way. In this two-part series, I'll show you how to create a RESTful API layer for your PHP application using a set of real best practices. The complete source code for this project will be provided at the end of Part 2.
Key Points
REST: Developer-friendly UI
First of all, the API is the developer's user interface, so it has to be friendly, simple, easy to use, and of course pleasant; otherwise, it will end up being another digital junk. Even if it's just a simple but well-written README file, the documentation is a good start. The least information we need is a summary of the service scope and a list of methods and access points. A good summary can be:> Our application is a simple contact list service for managing contacts with associated notes. It has two object types, contacts and notes. Each contact has basic attributes such as first name, last name, and email address. Additionally, each contact can have multiple notes in Markdown format associated with it.
Then, it's better to list all the resources and operations we're going to implement. This can be considered as an equivalent to visualizing the application wireframe. Following the key principles of REST, each resource is represented by a URL where the operation is the HTTP method used to access it. For example, GET /api/contacts/12 will retrieve a contact with ID 12, while PUT /api/contacts/12 will update the same contact. The complete method list is as follows:
<code>URL HTTP Method Operation /api/contacts GET 返回联系人数组 /api/contacts/:id GET 返回 ID 为 :id 的联系人 /api/contacts POST 添加一个新联系人并返回它(添加了 id 属性) /api/contacts/:id PUT 更新 ID 为 :id 的联系人 /api/contacts/:id PATCH 部分更新 ID 为 :id 的联系人 /api/contacts/:id DELETE 删除 ID 为 :id 的联系人 /api/contacts/:id/star PUT 将 ID 为 :id 的联系人添加到收藏夹 /api/contacts/:id/star DELETE 从收藏夹中删除 ID 为 :id 的联系人 /api/contacts/:id/notes GET 返回 ID 为 :id 的联系人的笔记 /api/contacts/:id/notes/:nid GET 返回 ID 为 :id 的联系人的 ID 为 :nid 的笔记 /api/contacts/:id/notes POST 为 ID 为 :id 的联系人添加新笔记 /api/contacts/:id/notes/:nid PUT 更新 ID 为 :id 的联系人的 ID 为 :nid 的笔记 /api/contacts/:id/notes/:nid PATCH 部分更新 ID 为 :id 的联系人的 ID 为 :nid 的笔记 /api/contacts/:id/notes/:nid DELETE 删除 ID 为 :id 的联系人的 ID 为 :nid 的笔记</code>
For more complete and professional documentation, you can consider using tools like Swagger, apiDoc, or Google APIs Discovery Service: Your users will like you!
Tools and Settings
The main tool I will use to build the API is the Slim Framework. Why? > [It] helps you write simple and powerful web applications and APIs quickly.
This is true. Its powerful routing capabilities make it easy to use methods other than GET and POST, it provides built-in support for HTTP method override (via HTTP headers and hidden POST fields) and can be hooked with middleware and extra features to enable applications Program and API development is really easy. Together with Slim, I use Idiorm to access the database layer and logging using Monolog. Therefore, our composer.json file will look like this:
{ "name": "yourname/my-contacts", "description": "Simple RESTful API for contacts management", "license": "MIT", "authors": [ { "name": "Your Name", "email": "you@yourdomain.com" } ], "require": { "slim/slim": "*", "slim/extras": "*", "slim/middleware": "*", "monolog/monolog": "*", "j4mie/paris": "*", "flynsarmy/slim-monolog": "*" }, "archive": { "exclude": ["vendor", ".DS_Store", "*.log"] }, "autoload": { "psr-0": { "API": "lib/" } } }
slim/extras and slim/middleware packages provide useful features such as content type resolution and basic authentication. Our custom class is located under the API namespace and in the lib directory. At this point, our working directory structure is as follows:
<code>bootstrap.php composer.json README.md bin/ import install lib/ API/ public/ .htaccess index.php share/ config/ default.php db/ logs/ sql/ data/ contacts.sql users.sql tables/ contacts.sql notes.sql users.sql ssl/ mysitename.crt mysitename.key</code>
The front-end controller of our application is public/index.php, and all non-file or directory traffic is redirected here via standard URL rewrite rules. Then I put all the initialization code in bootstrap.php and we'll see later. The share directory contains data such as logs, configuration files, SQLite databases and dump files, and SSL certificates. The bin directory contains utility scripts that use the provided .sql file to create a database and import some data.
Our API is accessible only in HTTPS mode and does not require redirection. This simplifies authentication logic and prevents improperly configured clients from accessing unencrypted endpoints. The easiest and most logical way to set up this method is to act directly on the web server or through a proxy server. I'm using old reliable Apache to do this, and my virtual host file looks like this:
<Directory> # Required for mod_rewrite in .htaccess AllowOverride FileInfo Options All -Indexes DirectoryIndex index.php index.shtml index.html <IfModule php5_module=""> # For Development only! php_flag display_errors On </IfModule> # Enable gzip compression <IfModule filter_module=""> AddOutputFilterByType DEFLATE application/json </IfModule> Order deny,allow Deny from all Allow from 127.0.0.1 </Directory> <VirtualHost *:80> ServerAdmin you@yourdomain.com DocumentRoot "/path/to/MyApp/public" ServerName myapp.dev <IfModule rewrite_module=""> RewriteEngine on ## Throw a 403 (forbidden) status for non secure requests RewriteCond %{HTTPS} off RewriteRule ^.*$ - [L,R=403] </IfModule> </VirtualHost> <IfModule ssl_module=""> NameVirtualHost *:443 Listen 443 SSLRandomSeed startup builtin SSLRandomSeed connect builtin <VirtualHost *:443> ServerAdmin you@yourdomain.com DocumentRoot "/path/to/MyApp/public" ServerName myapp.dev SSLEngine on SSLCertificateFile /path/to/MyApp/share/ssl/mysitename.crt SSLCertificateKeyFile /path/to/MyApp/share/ssl/mysitename.key SetEnv SLIM_MODE development </VirtualHost> </IfModule>
First define the directory settings so that they are common to the HTTP and HTTPS versions of our site. In a non-secure host configuration, I use mod_rewrite to issue a 403 forbid error for any non-secure connection, and then in the security section, I set up SSL with my self-signed certificate, as well as the SLIM_ENV variable that tells Slim the current application mode. For more information on how to create a self-signed certificate on Apache and install it, see this article on SSLShopper. Now that we have a clear goal, basic directory structure, and server settings, let's run composer.phar install and start writing some code.
Boot program and front-end controller
As mentioned earlier, the bootstrap.php file is responsible for loading our application settings and autoloader settings.
<code>URL HTTP Method Operation /api/contacts GET 返回联系人数组 /api/contacts/:id GET 返回 ID 为 :id 的联系人 /api/contacts POST 添加一个新联系人并返回它(添加了 id 属性) /api/contacts/:id PUT 更新 ID 为 :id 的联系人 /api/contacts/:id PATCH 部分更新 ID 为 :id 的联系人 /api/contacts/:id DELETE 删除 ID 为 :id 的联系人 /api/contacts/:id/star PUT 将 ID 为 :id 的联系人添加到收藏夹 /api/contacts/:id/star DELETE 从收藏夹中删除 ID 为 :id 的联系人 /api/contacts/:id/notes GET 返回 ID 为 :id 的联系人的笔记 /api/contacts/:id/notes/:nid GET 返回 ID 为 :id 的联系人的 ID 为 :nid 的笔记 /api/contacts/:id/notes POST 为 ID 为 :id 的联系人添加新笔记 /api/contacts/:id/notes/:nid PUT 更新 ID 为 :id 的联系人的 ID 为 :nid 的笔记 /api/contacts/:id/notes/:nid PATCH 部分更新 ID 为 :id 的联系人的 ID 为 :nid 的笔记 /api/contacts/:id/notes/:nid DELETE 删除 ID 为 :id 的联系人的 ID 为 :nid 的笔记</code>
First, I get the current environment. If a file named
{ "name": "yourname/my-contacts", "description": "Simple RESTful API for contacts management", "license": "MIT", "authors": [ { "name": "Your Name", "email": "you@yourdomain.com" } ], "require": { "slim/slim": "*", "slim/extras": "*", "slim/middleware": "*", "monolog/monolog": "*", "j4mie/paris": "*", "flynsarmy/slim-monolog": "*" }, "archive": { "exclude": ["vendor", ".DS_Store", "*.log"] }, "autoload": { "psr-0": { "API": "lib/" } } }
Configure a Monolog logger that writes to the file of app/path/share/logs/EnvName_YYYY-mm-dd.log. Then, after some improvements (you can see them in the source code), I get the generated log writer and try to connect to the database:
<code>bootstrap.php composer.json README.md bin/ import install lib/ API/ public/ .htaccess index.php share/ config/ default.php db/ logs/ sql/ data/ contacts.sql users.sql tables/ contacts.sql notes.sql users.sql ssl/ mysitename.crt mysitename.key</code>
Finally, I added the required middleware to my application instance. Slim's middleware is like an onion layer, the first middleware you add will be the innermost layer, so the order of our middleware is important. I use the following middleware in our API: - Cache (inner level); - ContentTypes: parse the body of JSON format from the client; - RateLimit: manages the user's API limits; - JSON: "JSON response only" and " JSON encoding body"Best practice utility middleware; - Authentication (outermost layer). We will write all of this, except for the pre-existing ContentTypes. At the end of the bootstrap file, I define two global variables $app (ap) and $log (log writer). The file is loaded by our front-end controller index.php, and something magic happens in that file.
Routing structure
Slim has a nice feature called Route Groups. Using this feature, we can define our application routes like this:
<Directory> # Required for mod_rewrite in .htaccess AllowOverride FileInfo Options All -Indexes DirectoryIndex index.php index.shtml index.html <IfModule php5_module=""> # For Development only! php_flag display_errors On </IfModule> # Enable gzip compression <IfModule filter_module=""> AddOutputFilterByType DEFLATE application/json </IfModule> Order deny,allow Deny from all Allow from 127.0.0.1 </Directory> <VirtualHost *:80> ServerAdmin you@yourdomain.com DocumentRoot "/path/to/MyApp/public" ServerName myapp.dev <IfModule rewrite_module=""> RewriteEngine on ## Throw a 403 (forbidden) status for non secure requests RewriteCond %{HTTPS} off RewriteRule ^.*$ - [L,R=403] </IfModule> </VirtualHost> <IfModule ssl_module=""> NameVirtualHost *:443 Listen 443 SSLRandomSeed startup builtin SSLRandomSeed connect builtin <VirtualHost *:443> ServerAdmin you@yourdomain.com DocumentRoot "/path/to/MyApp/public" ServerName myapp.dev SSLEngine on SSLCertificateFile /path/to/MyApp/share/ssl/mysitename.crt SSLCertificateKeyFile /path/to/MyApp/share/ssl/mysitename.key SetEnv SLIM_MODE development </VirtualHost> </IfModule>
I created two nested groups /api and /v1 so we can easily adhere to the "version control in URL" best practice. I also created some optional routes for /api/ which may contain user-readable content, as well as a common root URL (/) URL that in the real world may contain the public user interface of the application.
My initial approach was to use routing middleware (another Slim middleware) within the /v1 group for authentication and JSON request/response, but I found it more practical and concise to use classic middleware. As mentioned earlier, middleware is an instance of a class inherited from SlimMiddleware. The call() method of the Slim middleware is where the operation occurs. When the middleware is linked as a global middleware, it will be executed automatically, using the $app->add() method.
// Init application mode if (empty($_ENV['SLIM_MODE'])) { $_ENV['SLIM_MODE'] = (getenv('SLIM_MODE')) ? getenv('SLIM_MODE') : 'development'; } // Init and load configuration $config = array(); $configFile = dirname(__FILE__) . '/share/config/' . $_ENV['SLIM_MODE'] . '.php'; if (is_readable($configFile)) { require_once $configFile; } else { require_once dirname(__FILE__) . '/share/config/default.php'; } // Create Application $app = new API\Application($config['app']);
Our JSON middleware implements two best practices: "JSON response only" and "JSON encoding body". The method is as follows:
<code>URL HTTP Method Operation /api/contacts GET 返回联系人数组 /api/contacts/:id GET 返回 ID 为 :id 的联系人 /api/contacts POST 添加一个新联系人并返回它(添加了 id 属性) /api/contacts/:id PUT 更新 ID 为 :id 的联系人 /api/contacts/:id PATCH 部分更新 ID 为 :id 的联系人 /api/contacts/:id DELETE 删除 ID 为 :id 的联系人 /api/contacts/:id/star PUT 将 ID 为 :id 的联系人添加到收藏夹 /api/contacts/:id/star DELETE 从收藏夹中删除 ID 为 :id 的联系人 /api/contacts/:id/notes GET 返回 ID 为 :id 的联系人的笔记 /api/contacts/:id/notes/:nid GET 返回 ID 为 :id 的联系人的 ID 为 :nid 的笔记 /api/contacts/:id/notes POST 为 ID 为 :id 的联系人添加新笔记 /api/contacts/:id/notes/:nid PUT 更新 ID 为 :id 的联系人的 ID 为 :nid 的笔记 /api/contacts/:id/notes/:nid PATCH 部分更新 ID 为 :id 的联系人的 ID 为 :nid 的笔记 /api/contacts/:id/notes/:nid DELETE 删除 ID 为 :id 的联系人的 ID 为 :nid 的笔记</code>
We can pass the root path to the middleware constructor. In this case, I pass /api/v1 so that our middleware is only applied to the API part of our site. If the current path matches the response content type header, the response content type header is forced to be application/json, and I check the request method. If the request method is one of the request methods that enable writes (PUT, POST, PATCH), the request content type header must be application/json, otherwise the application will exit and display the 415 Unsupported Media Type HTTP status code. If everything works fine, the statement $this->next->call() will run the next middleware in the chain.
Authentication
Since our application will run on HTTPS by default, I decided to use a method where tokens take precedence over basic authentication: API keys are sent to the username field of the basic HTTP AUTH header (no password required) ). To do this, I wrote a Slim middleware class called TokenOverBasicAuth by modifying the existing Slim HttpBasicAuth. This middleware runs first in the chain, so it is added as the last one, and it takes an optional root path parameter in the constructor.
{ "name": "yourname/my-contacts", "description": "Simple RESTful API for contacts management", "license": "MIT", "authors": [ { "name": "Your Name", "email": "you@yourdomain.com" } ], "require": { "slim/slim": "*", "slim/extras": "*", "slim/middleware": "*", "monolog/monolog": "*", "j4mie/paris": "*", "flynsarmy/slim-monolog": "*" }, "archive": { "exclude": ["vendor", ".DS_Store", "*.log"] }, "autoload": { "psr-0": { "API": "lib/" } } }
This method searches the PHP_AUTH_USER request header for auth token, and if it does not exist or is invalid, passes the 401 prohibited status and authentication header to the client. The verify() method is protected and therefore can be overridden by subclasses; my version here is simple:
<code>bootstrap.php composer.json README.md bin/ import install lib/ API/ public/ .htaccess index.php share/ config/ default.php db/ logs/ sql/ data/ contacts.sql users.sql tables/ contacts.sql notes.sql users.sql ssl/ mysitename.crt mysitename.key</code>
Here, I just check the existence of the API key in the users table and if I find a valid user, it is added to the application context for use with the next layer (RateLimit). You can modify or extend this class to inject your own authentication logic or use the OAuth module. For more information about OAuth, see Jamie Munro’s article.
Used error payload
Our API should display useful error messages in a usable format, preferably in JSON representation, if possible. We need a minimum payload containing error codes and messages. Additionally, verification errors require more segmentation. Using Slim, we can redefine 404 errors and server errors using the $app->notFound() and $app->error() methods, respectively.
<code>URL HTTP Method Operation /api/contacts GET 返回联系人数组 /api/contacts/:id GET 返回 ID 为 :id 的联系人 /api/contacts POST 添加一个新联系人并返回它(添加了 id 属性) /api/contacts/:id PUT 更新 ID 为 :id 的联系人 /api/contacts/:id PATCH 部分更新 ID 为 :id 的联系人 /api/contacts/:id DELETE 删除 ID 为 :id 的联系人 /api/contacts/:id/star PUT 将 ID 为 :id 的联系人添加到收藏夹 /api/contacts/:id/star DELETE 从收藏夹中删除 ID 为 :id 的联系人 /api/contacts/:id/notes GET 返回 ID 为 :id 的联系人的笔记 /api/contacts/:id/notes/:nid GET 返回 ID 为 :id 的联系人的 ID 为 :nid 的笔记 /api/contacts/:id/notes POST 为 ID 为 :id 的联系人添加新笔记 /api/contacts/:id/notes/:nid PUT 更新 ID 为 :id 的联系人的 ID 为 :nid 的笔记 /api/contacts/:id/notes/:nid PATCH 部分更新 ID 为 :id 的联系人的 ID 为 :nid 的笔记 /api/contacts/:id/notes/:nid DELETE 删除 ID 为 :id 的联系人的 ID 为 :nid 的笔记</code>
The error is not found simpler: First I get the requested media type, and then the $isAPI flag tells me if the current URL is under the /api/v* group. If the client requests the API URL or sends a JSON content type header, I will return the JSON output, otherwise I can render the template or simply print some static HTML as shown in this example. Other errors are a bit tricky, and the $app->error() method is triggered when an exception occurs, and Slim converts a standard PHP error to an ErrorException object. We need a way to provide useful errors to clients without exposing too many internal mechanisms to avoid security vulnerabilities. For this application, I created two custom exceptions, APIException and APIExceptionValidationException, which are exposed to the public, all other exception types are logged in the log and are displayed only in development mode.
{ "name": "yourname/my-contacts", "description": "Simple RESTful API for contacts management", "license": "MIT", "authors": [ { "name": "Your Name", "email": "you@yourdomain.com" } ], "require": { "slim/slim": "*", "slim/extras": "*", "slim/middleware": "*", "monolog/monolog": "*", "j4mie/paris": "*", "flynsarmy/slim-monolog": "*" }, "archive": { "exclude": ["vendor", ".DS_Store", "*.log"] }, "autoload": { "psr-0": { "API": "lib/" } } }
$app->error() method receives the thrown exception as a parameter. By default I get all the data I need and populate the $error array, and then if I'm in production mode I'll unset the private data and rewrite the message with the general data. The custom ValidationException class has a custom getData() method that returns an array of validation errors added to the final payload. Then, display the error in JSON or HTML based on the request. On the API side, we can have a simple error as follows:
<code>bootstrap.php composer.json README.md bin/ import install lib/ API/ public/ .htaccess index.php share/ config/ default.php db/ logs/ sql/ data/ contacts.sql users.sql tables/ contacts.sql notes.sql users.sql ssl/ mysitename.crt mysitename.key</code>
or a complete verification error as shown below:
<Directory> # Required for mod_rewrite in .htaccess AllowOverride FileInfo Options All -Indexes DirectoryIndex index.php index.shtml index.html <IfModule php5_module=""> # For Development only! php_flag display_errors On </IfModule> # Enable gzip compression <IfModule filter_module=""> AddOutputFilterByType DEFLATE application/json </IfModule> Order deny,allow Deny from all Allow from 127.0.0.1 </Directory> <VirtualHost *:80> ServerAdmin you@yourdomain.com DocumentRoot "/path/to/MyApp/public" ServerName myapp.dev <IfModule rewrite_module=""> RewriteEngine on ## Throw a 403 (forbidden) status for non secure requests RewriteCond %{HTTPS} off RewriteRule ^.*$ - [L,R=403] </IfModule> </VirtualHost> <IfModule ssl_module=""> NameVirtualHost *:443 Listen 443 SSLRandomSeed startup builtin SSLRandomSeed connect builtin <VirtualHost *:443> ServerAdmin you@yourdomain.com DocumentRoot "/path/to/MyApp/public" ServerName myapp.dev SSLEngine on SSLCertificateFile /path/to/MyApp/share/ssl/mysitename.crt SSLCertificateKeyFile /path/to/MyApp/share/ssl/mysitename.key SetEnv SLIM_MODE development </VirtualHost> </IfModule>
Conclusion
We now have the core of the API. In the next section, we will add some content to have a fully functional service. During this time, feel free to read the articles linked in this section – they are a treasure trove of useful API design principles.
FAQs (FAQ) on building REST APIs from scratch
REST API consists of several key components. First is the HTTP method, which defines the type of operation to be performed. These include GET, POST, PUT, DELETE, etc. The second component is a URL or URI, which is the resource identifier. The third component is the HTTP header, which carries the metadata of HTTP requests and responses. The fourth component is the body or payload, which carries the actual data to be transmitted. Finally, the status code indicates the success or failure of the HTTP request.
Protecting your REST API is essential to protecting sensitive data. You can use various methods such as API keys, OAuth, or JWT for authentication and authorization. Additionally, data transfer is always used to ensure data integrity and confidentiality. Regularly update and patch your API and its dependencies to protect against vulnerabilities.
Versioning your REST API allows you to introduce non-destructive changes without affecting existing clients. You can version the API by including the version number in the URL or using a custom request header. Remember to log all changes and inform your API consumers of the new version and their features.
Correct error handling in the REST API improves its usability and reliability. Use an HTTP status code to indicate the error type. Include an error message in the response body for more details about the error. This helps the client understand what is wrong and how to solve the problem.
Test your REST API to ensure it works as expected and can handle a variety of scenarios. You can use tools like Postman or curl for manual testing. For automated testing, consider using unit testing, integration testing, and end-to-end testing. Use a mock server to simulate API responses and test how your API handles different types of responses.
Good documentation makes your REST API easy to understand and use. Includes detailed information about endpoints, request methods, request parameters, request examples, response status codes, and response examples. You can use tools like Swagger or Postman to generate and host your API documents.
Design RESTful APIs involve planning resources, endpoints, and methods. Use nouns for resources and HTTP methods for operations. Keep the API simple and intuitive. Use status codes to indicate the result of the request. Make your API stateless, which means that each request should contain all the information you need to process the request.
Paging helps limit the amount of data returned in a single response. You can implement paging using query parameters such as "page" and "limit". Include metadata in the response header or body to indicate the current page, total number of pages, total number of items, etc.
Rate limiting protects your REST API from abuse and ensures fair use. You can limit the number of requests based on your IP address, API key, or user account. Use HTTP headers to communicate the rate limiting status to the client.
You can deploy your REST API to a server or cloud platform. When selecting deployment options, consider factors such as cost, scalability, and security. Use Continuous Integration and Continuous Delivery (CI/CD) tools to automate the deployment process. Monitor your API performance and usage to ensure it meets the needs of your users.
The above is the detailed content of Build a REST API from Scratch: An Introduction. For more information, please follow other related articles on the PHP Chinese website!