REST: A pleasant UI for developers
First of all, an API is a user interface for developers, so it must be friendly, simple, easy to use and of course pleasant; or else it will end up being another piece of digital junk out there. Documentation, even in the form of a simple but well writtenREADME
file, is a good place to start. The minimal information we need is a summary of the service’s scope and the list of methods and access points.
A good summary can be:
Our application is a simple contact list service that manages contacts with linked notes. It has two object types, contacts and notes. Each contact has basic attributes such as first name, last name, and email address. Also, each contact can have a number of markdown-formatted notes linked to it.Then, it’s a good idea to make a list of all the resources and actions that we are going to implement. This can be seen as the equivalent of wireframing for visual applications. Following the key principles of REST, each resource is represented by a URL, where the action is the HTTP method used to access it. For example
GET /api/contacts/12
retrieves the contact with id
of 12, while PUT /api/contacts/12
will update that same contact.
The full list of methods is displayed below:
URL HTTP Method Operation
/api/contacts GET Returns an array of contacts
/api/contacts/:id GET Returns the contact with id of :id
/api/contacts POST Adds a new contact and return it with an id attribute added
/api/contacts/:id PUT Updates the contact with id of :id
/api/contacts/:id PATCH Partially updates the contact with id of :id
/api/contacts/:id DELETE Deletes the contact with id of :id
/api/contacts/:id/star PUT Adds to favorites the contact with id of :id
/api/contacts/:id/star DELETE Removes from favorites the contact with id of :id
/api/contacts/:id/notes GET Returns the notes for the contact with id of :id
/api/contacts/:id/notes/:nid GET Returns the note with id of :nid for the contact with id of :id
/api/contacts/:id/notes POST Adds a new note for the contact with id of :id
/api/contacts/:id/notes/:nid PUT Updates the note with id if :nid for the contact with id of :id
/api/contacts/:id/notes/:nid PATCH Partially updates the note with id of :nid for the contact with id of :id
/api/contacts/:id/notes/:nid DELETE Deletes the note with id of :nid for the contact with id of :id
For a more complete and professional documentation, you may consider some tools like Swagger, apiDoc or Google APIs Discovery Service: your users will love you!
Tools and setup
The main tool I’m going to use to build our API is Slim Framework. Why?[It] helps you quickly write simple yet powerful web applications and APIs.And that’s true. Its powerful routing feature makes it easy to use methods other than the standard
GET
and POST
, it provides built-in support for HTTP method overridess (via both HTTP header and hidden POST fields) and it can be hooked with middleware and extra features to make apps and API development really easy.
Along with Slim I’m using Idiorm to access the database layer and Monolog for logging. So 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/"
}
}
}
The slim/extras
and slim/middleware
packages provide useful features such as content type parsing and basic authentication. Our custom classes are under the namespace of API
and sit inside the lib
directory.
At this point our working directory structure would look like this:
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
The front controller of our application is public/index.php
, where all the non-file-or-directory traffic is redirected via standard URL rewrite rules.
I’ve then placed all the initialization code in bootstrap.php
which we’ll see later. The share
directory contains data such as logs, configuration files, the SQLite databases and dump files, and the SSL certificates. The bin
directory contains utility scripts that create the database and import some data using the provided .sql
files.
SSL Everywhere
Our API will be accessible only in HTTPS mode, with no redirection. This simplifies the authentication logic and prevents poorly configured clients to access non encrypted endpoints. The easiest and more logical way to set this up is acting directly on the web server or through a proxy server. I’m using old trusted Apache to do this, and my the virtual host file looks like this:<Directory "/path/to/MyApp/public">
# 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>
Directory settings are defined first, so that they are in common with both the HTTP and HTTPS version of our site. In the non-secure host configuration I’m using mod_rewrite
to issue a 403 Forbidden
error for any non secure connection, then in the secure section I’m setting up SSL using my self-signed certificates, along with the SLIM_ENV
variable that tells Slim the current application mode.
For more information on how to create a self signed certificate an install it on your Apache see this article on SSLShopper.
Now that we have clear objectives, a basic directory structure, and a server setup, let’s run a composer.phar install
and start writing some code.
Bootstrap and Front Controller
As said before, thebootstrap.php
file is responsible for loading our application settings and autoloader setup.
// 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']);
First I get the current environment. If a file named <EnvName>.php
is present it’s loaded, if not the default config file is loaded. Slim specific settings are stored in the $config['app']
array and passed to the constructor of our application that extends the basic Slim object (optional but recommended).
For example the statement:
$config['app']['log.writer'] = new \Flynsarmy\SlimMonolog\Log\MonologWriter(array(
'handlers' => array(
new \Monolog\Handler\StreamHandler(
realpath(__DIR__ . '/../logs')
.'/'.$_ENV['SLIM_MODE'] . '_' .date('Y-m-d').'.log'
),
),
));
configures a Monolog logger that writes to a file at app/path/share/logs/EnvName_YYYY-mm-dd.log
.
Then, after some refinements (you can see them in the source), I get the generated log writer and try to connect to the database:
// Get log writer
$log = $app->getLog();
// Init database
try {
if (!empty($config['db'])) {
\ORM::configure($config['db']['dsn']);
if (!empty($config['db']['username']) && !empty($config['db']['password'])) {
\ORM::configure('username', $config['db']['username']);
\ORM::configure('password', $config['db']['password']);
}
}
} catch (\PDOException $e) {
$log->error($e->getMessage());
}
// Add middleware
// Your middleware here...
$app->add(new Some\Middleware\Class(...));
Lastly I add the needed middleware to my application instance. Slim’s middleware is like the layers of an onion, the first middleware you add will be the innermost layer, so the order of our middleware is important. I’m using the following middleware in our API:
- Cache (the innermost);
- ContentTypes: parses JSON-formatted body from the client;
- RateLimit: manages users API limits;
- JSON: utility middleware for “JSON only responses” and “JSON encoded bodies” best practices;
- Authentication (outermost).
ContentTypes
.
At the end of the bootstrap file I’ve defined the two global variables $app
(the application instance) and $log
(the log writer). The file is loaded by our front controller index.php
where the magic happens.
Routing structure
Slim has a nice feature called Route Groups. With this feature we can define our application routes like this:// API group
$app->group('/api', function () use ($app, $log) {
// Version group
$app->group('/v1', function () use ($app, $log) {
// GET route
$app->get('/contacts/:id', function ($id) use ($app, $log) {
});
// PUT route, for updating
$app->put('/contacts/:id', function ($id) use ($app, $log) {
});
// DELETE route
$app->delete('/contacts/:id', function ($id) use ($app, $log) {
});
});
});
I’ve created two nested groups, /api
and /v1
, so we can easily adhere to the “versioning in the URL” best practice. I’ve also created some optional routes for /api/
, that could contain user-readable content, and a generic root URL (/
) URL that in the real world could contain the public user interface for the app.
JSON middleware
My initial approach here was using a route middleware (another type of Slim’s middleware) inside the/v1
group for authentication and JSON requests/responses, but I’ve found it is more practical and clean to use classic middleware. As seen previously, a middleware is an instance of a class that inherits from Slim\Middleware
. The call()
method of a Slim middleware is where the action takes place, it’s executed automatically when the middleware is linked as global, with the $app->add()
method.
$app->add(new API\Middleware\JSON('/api/v1'));
Our JSON middleware achieves two best practices: “JSON only responses” and “JSON encoded bodies”. Here’s how:
// JSON middleware call
public function call()
{
if (preg_match('|^' . $this->root . '.*|', $this->app->request->getResourceUri())) {
// Force response headers to JSON
$this->app->response->headers->set(
'Content-Type',
'application/json'
);
$method = strtolower($this->app->request->getMethod());
$mediaType = $this->app->request->getMediaType();
if (in_array(
$method,
array('post', 'put', 'patch')
)) {
if (empty($mediaType)
|| $mediaType !== 'application/json') {
$this->app->halt(415);
}
}
}
$this->next->call();
}
We can pass a root path to our middleware constructor. In this case I’m passing /api/v1
so that our middleware is applied only on the API part of our site. If the current path matches the response content type header is forced to application/json
, then I check the request method. If the request method is one of the write-enabled ones (PUT
, POST
, PATCH
) the request content type header must be application/json
, if not the application exits with a 415 Unsupported Media Type
HTTP status code.
If all is right the statement $this->next->call()
runs the next middleware in the chain.
Authentication
Since our application will run on HTTPS by default, I decided to use the token-over-basic-authentication approach: an API key is sent in the username field of the basic HTTP AUTH headers (no password required). In order to do this I wrote a Slim middleware class calledTokenOverBasicAuth
, by modifying the existing Slim HttpBasicAuth
. This middleware is run first in the chain so it is added as last, and it takes an optional root path parameter in the constructor.
// Auth middleware call
public function call()
{
$req = $this->app->request();
$res = $this->app->response();
if (preg_match('|^' . $this->config['root'] . '.*|', $req->getResourceUri())) {
// We just need the user
$authToken = $req->headers('PHP_AUTH_USER');
if (!($authToken && $this->verify($authToken))) {
$res->status(401);
$res->header(
'WWW-Authenticate',
sprintf('Basic realm="%s"', $this->config['realm'])
);
}
}
$this->next->call();
}
The method searches for the auth token within the PHP_AUTH_USER
request header, if it does not exist or is invalid a 401 Forbidden
status and authentication headers are passed to the client. The verify()
method is protected so that it can be overridden by child classes; my version here is simple:
protected function verify($authToken)
{
$user = \ORM::forTable('users')->where('apikey', $authToken)
->findOne();
if (false !== $user) {
$this->app->user = $user->asArray();
return true;
}
return false;
}
Here I’m simply checking for the presence of the API key in the users
table, and If I find a valid user it’s added to the application context to be used with the next layer (RateLimit). You can modify or extend this class to inject your own authentication logic or use an OAuth module. For more info on OAuth see Jamie Munro’s article.
Consumable error payloads
Our API should show useful error messages in consumable format, if possible in JSON representation. We need a minimal payload that contains an error code and message. In addition, validation errors require more breakdown. With Slim we can redefine both404
errors and server errors with the $app->notFound()
and $app->error()
method respectively.
$app->notFound(function () use ($app) {
$mediaType = $app->request->getMediaType();
$isAPI = (bool) preg_match('|^/api/v.*$|', $app->request->getPath());
if ('application/json' === $mediaType || true === $isAPI) {
$app->response->headers->set(
'Content-Type',
'application/json'
);
echo json_encode(
array(
'code' => 404,
'message' => 'Not found'
)
);
} else {
echo '<html>
<head><title>404 Page Not Found</title></head>
<body><h1>404 Page Not Found</h1><p>The page you are
looking for could not be found.</p></body></html>';
}
});
Not found errors are simpler: first I’m grabbing the media type of the request, then the $isAPI
flag tells me if the current URL is under the /api/v*
group. If the client requested an API URL or sent a JSON content type header I’m returning a JSON output, else I can render a template or simply print some static HTML, as in this example.
Other errors are a little bit tricky, the $app->error()
method is triggered when there is an exception and Slim transforms standard PHP errors in ErrorException
objects. We need a way to give a useful error to the client without exposing too much of our internal mechanism in order to avoid security flaws. I’ve created two custom exceptions for this application, an API\Exception
and an API\Exception\ValidationException
that are exposed to the public, all other exception types are logged, and displayed only in development mode.
$app->error(function (\Exception $e) use ($app, $log) {
$mediaType = $app->request->getMediaType();
$isAPI = (bool) preg_match('|^/api/v.*$|', $app->request->getPath());
// Standard exception data
$error = array(
'code' => $e->getCode(),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
);
// Graceful error data for production mode
if (!in_array(
get_class($e),
array('API\\Exception', 'API\\Exception\ValidationException')
)
&& 'production' === $app->config('mode')) {
$error['message'] = 'There was an internal error';
unset($error['file'], $error['line']);
}
// Custom error data (e.g. Validations)
if (method_exists($e, 'getData')) {
$errors = $e->getData();
}
if (!empty($errors)) {
$error['errors'] = $errors;
}
$log->error($e->getMessage());
if ('application/json' === $mediaType || true === $isAPI) {
$app->response->headers->set(
'Content-Type',
'application/json'
);
echo json_encode($error);
} else {
echo '<html>
<head><title>Error</title></head>
<body><h1>Error: ' . $error['code'] . '</h1><p>'
. $error['message']
.'</p></body></html>';
}
});
The $app->error()
method receives the thrown exception as argument. By default I fetch all the data I need and fill the $error
array, then if I’m in production mode I unset the private data and rewrite the message with a generic one. The custom ValidationException
class has a custom getData()
method that returns an array of validation errors that are added to the final payload. Then the error is displayed in JSON or HTML depending on the request. On the API side we can have a simple error like this:
{
"code": 123,
"message": "An error occurred, a support person is being notified of this"
}
or a full validation error like this:
{
"code": 0,
"message": "Invalid data",
"errors":
{
"contact":
[
{
"field": "email",
"message": "Email address already exists"
}
]
}
}
Conclusion
We have the core of our API in place now. In the next part we’ll add some flesh in order to have a full functioning service. In the meanwhile, feel free to read through the articles linked throughout this part – they’re a treasure trove of useful API design principles.Frequently Asked Questions (FAQs) about Building a REST API from Scratch
What are the key components of a REST API?
A REST API consists of several key components. The first is the HTTP methods, which define the type of operation to be performed. These include GET, POST, PUT, DELETE, and others. The second component is the URL or URI, which is the resource identifier. The third component is the HTTP headers, which carry metadata for the HTTP request and response. The fourth component is the body or payload, which carries the actual data to be transmitted. Lastly, the status codes indicate the success or failure of the HTTP request.
How do I secure my REST API?
Securing your REST API is crucial to protect sensitive data. You can use various methods such as API keys, OAuth, or JWT for authentication and authorization. Additionally, always use HTTPS for data transmission to ensure data integrity and confidentiality. Regularly update and patch your API and its dependencies to protect against vulnerabilities.
How do I version my REST API?
Versioning your REST API allows you to introduce non-breaking changes without affecting existing clients. You can version your API by including the version number in the URL or use custom request headers. Remember to document all changes and inform your API consumers about the new version and its features.
How do I handle errors in a REST API?
Proper error handling in a REST API improves its usability and reliability. Use HTTP status codes to indicate the type of error. Include an error message in the response body for more details about the error. This helps the client understand what went wrong and how to fix it.
How do I test my REST API?
Testing your REST API ensures it works as expected and can handle various scenarios. You can use tools like Postman or curl for manual testing. For automated testing, consider using unit tests, integration tests, and end-to-end tests. Use mock servers to simulate API responses and test how your API handles different types of responses.
How do I document my REST API?
Good documentation makes your REST API easy to understand and use. Include details about the 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 documentation.
How do I design a RESTful API?
Designing a RESTful API involves planning the resources, endpoints, and methods. Use nouns for resources and HTTP methods for actions. Keep your API simple and intuitive. Use status codes to indicate the result of the request. Make your API stateless, meaning each request should contain all the information needed to process the request.
How do I paginate results in a REST API?
Pagination helps to limit the amount of data returned in a single response. You can implement pagination using query parameters like ‘page’ and ‘limit’. Include metadata in the response headers or body to indicate the current page, total pages, total items, etc.
How do I rate limit my REST API?
Rate limiting protects your REST API from abuse and ensures fair usage. You can limit the number of requests based on the IP address, API key, or user account. Use HTTP headers to communicate the rate limit status to the client.
How do I deploy my REST API?
You can deploy your REST API on a server or a cloud platform. Consider factors like cost, scalability, and security when choosing a deployment option. Use continuous integration and continuous deployment (CI/CD) tools to automate the deployment process. Monitor your API performance and usage to ensure it meets your users’ needs.
Vito Tardia (a.k.a. Ragman), is a web designer and full stack developer with 20+ years experience. He builds websites and applications in London, UK. Vito is also a skilled guitarist and music composer and enjoys writing music and jamming with local (hard) rock bands. In 2019 he started the BlueMelt instrumental guitar rock project.