Modern REST API Tutorial in Python

This is a practical, hands-on Python REST API tutorial in plain English, written for modern developers who need to build real B2B solutions, based on real-world experience and know-how from the trenches.

What you'll find here are the details of current best practices, of what works now in REST APIs, and how to build API-first systems in the contemporary world.

What you won't find here are obscure history lessons, theoretical findings from 20 years ago, or other irrelevant technical details that have little bearing on what API integrations actually look like today.

How This Tutorial Works

  • Learn like a senior developer - plain-English explanations, zero unnecessary complexity

  • Practice-driven learning - dive into practical, hands-on development using battle-tested patterns

  • Production-ready results in 60 minutes - everything you need to build professional Python APIs

REST API Building Blocks and How REST APIs Work

  • Remember that API stands for an Application Programming Interface. In other words, it's just a way for an application - or a database, a system - to expose its functionality to other interested parties.

  • API clients is the term under which the interested parties are known, even though each API client can be another backend system, or an actual client application. In other words, an "API client" means "that which invokes an API", no matter who or what it actually is. It could be an external application, but it could very well be another part of the same application.

  • An API endpoint is an individual element of an API that clients invoke. So, let's say you build an API for an employee database - as you'll be doing in this tutorial - and this interface will let clients manage employee data. Each such action will be exposed to clients as an individual API endpoint.

  • REST used to be expanded into "representational state transfer" but practically no one uses this term anymore, and very few people actually were using it back when it was coined.

  • What matters today is that REST is an architecture, a set of guidelines, patterns and best practices driving the design of robust APIs that use HTTP to offer interfaces to API clients.

An example REST API endpoint will be this one:

https://zato.io/tutorial/api/user/123

This endpoint, this URL, lets you access information about a particular user. As an API client, you send an HTTP request and the server returns a response about a specific resource, which in this case is a user whose ID is 123.

  • Behind each endpoint is an API service. This is the actual code that reacts to requests and which produces responses to clients. We're going to write API services in Python in this tutorial.

  • CRUD is a related term. It means Create, Read, Update and Delete and these are the most commonly seen operations that you carry out on your resources. I.e. you typically create a user, read the details of a user, and then you often update user details and sometimes delete a user as well.

Questions and Answers on API Terminology

  • Is there a difference between REST APIs and APIs? Yes and no. APIs can be implemented in many ways, and the term itself has been around for decades, before anyone thought of REST or the Internet itself.

    But, when talking about backend systems, the word "REST" is often implied. So, know that "API" has a much broader meaning than just "REST API", but when you and the people you're talking with understand the context, you'll often shorten "REST API" to "API" alone.

    For instance, you can say both e.g. "I'll create an API" or "We need a set of REST APIs". Either is fine. Similarly, an "API client" is often shortened to "client" when the context is clear, while an "API endpoint" becomes an "endpoint".

    What never works though is "I'll add a REST" or "Let's invoke their REST". That doesn't sound right at all.

  • Similarly, you'll talk about "clients and servers" without always adding "API". And remember, one server can be an API client of another server, the roles are relative.

  • You may hear about "web services" from time to time and the concept is essentially the same - you have a client, server, a request and response, and the parties communicate using HTTP. The term as such is much older and more generic than REST, but today they're synonymous because most web services use REST, so when you say "we're building web services", you'll be almost certainly understood and the meaning will be "we're building REST APIs".

  • Is there a difference between a "Web API" and "REST API"? Not really. While both terms describe HTTP-based APIs, "Web API" is less commonly used and suggests public Internet services. "REST API" is the preferred term, especially when describing internal or enterprise integrations.

Prerequisites and Setting Up

This tutorial uses Zato so go here, install the platform, and log in to your dashboard at http://localhost:8183 (username: admin).

You probably use VS Code, so you'll be interested in the Zato plug-in for that IDE.

But Zato comes with its own IDE too, so once you're logged in to the Dashboard, you can navigate to Services → IDE, and you'll be able to follow the tutorial in this way too.

Creating a REST API Service in Python

Let's quickly create an API service. Save the below in a file called "api.py":

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetEmployee(Service):

    def get_data(self):

        # Our sample data ..
        data = {'name':'John Doe', 'employee_no':123, 'department':'HR'}

        # .. which we can return to our caller..
        return data

    def handle(self):

        # Get our data ..
        data = self.get_data()

        # .. and return a response to the calling API client.
        self.response.payload = data

Now, we need a REST endpoint for it. Go to Connections → Channels → REST, click "Create a new REST channel", and fill out the form as below.

Accessing a Resource

Invoke the newly created endpoint using the GET HTTP method:

curl -X GET localhost:17010/api/employee/get/123
{"name":"John Doe","employee_no":123,"department":"HR"}

That's the gist of it. Now, let's talk more about REST principles, what this architectural style is about and the details of what it offers.

Afterwards, we'll create a full Employee API in Python, use Postman to invoke it, and we'll also write API tests in plain English.

HTTP Methods in Practice

Clients may potentially use HTTP methods to tell your services what they want to happen with a given resource - for instance, whether they would do GET the details of it, or to the contrary, whether they want to DELETE it.

For instance, you can design your APIs that will let the clients differentiate between the two:

curl -X GET localhost:17010/api/employee/123
curl -X DELETE localhost:17010/api/employee/123

While elegant in theory, real-world implementations rarely follow such a clean pattern because real-world business processes and requirements rarely fit into such neat boxes with simple request-response cycles.

Nevertheless, the bare minimum when choosing which HTTP method to use should be:

MethodDescription
GETGET is for reading, nothing else. It should return the requested resource - whether that's a single employee or a list of employees with their office locations - without modifying any business data. Never use a GET request as a trigger for other operations. If someone requests an employee record, just return it. Don't use this as an opportunity to clean up old data, remove unrelated archive files, or trigger background jobs. These kinds of side effects belong in different endpoints with appropriate HTTP methods.
DELETEDELETE makes resources inaccessible. Whether it's a hard delete (removing database records) or a soft delete (marking records as inactive), the end result is the same: the resource becomes inaccessible through the API.
POSTPOST can be used as a catch-all method for any other operation. Unlike its more specialized siblings, POST can handle any type of action. Whether you're creating resources or running complex business processes, POST works. But use this flexibility wisely - if another HTTP method fits your use case better, prefer that instead.

GET is the only method you'll realistically cache in production.

Take employee data as an example: since it rarely changes, your response from "GET /api/employee/123" can be safely cached for hours or even days.

Instead of hitting the database on every request, you can serve employee details from cache, significantly reducing database load and response times.

More about HTTP Methods

You may come across two other HTTP methods, although they aren't used that commonly:

MethodDescription
PUTPUT completely updates an existing resource.
PATCHPATCH partially updates an existing resource.

This sounds straightforward but the reality is messier than this idealized model suggests, and it's not always easy to say where to use which one, which in turn leads to confusion among the users of your APIs, and to completely unnecessary discussions among the teams that create them.

The boundaries between PUT and PATCH often blur in real-world APIs, leading to two problems: API clients struggle to predict the right method to use, and API design discussions get bogged down in lengthy debates about method choice. While the theory is elegant, practical implementation decisions require trade-offs.

Default to POST when the choice isn't obvious. It's the accepted catch-all for data modifications, and every developer understands this convention. Save your energy for solving real business problems rather than debating HTTP method semantics. Your API clients will understand the intent: if it's not reading (GET) or removing (DELETE) data, POST is the safe choice.

The DELETE method itself raises interesting questions too. Consider soft deletion: if you're just marking an employee as inactive while keeping their records accessible to admins for six months, is DELETE the right choice? Or should you PATCH the status field instead? And when do you perform the actual deletion?

This ambiguity explains why it's often better to opt for a simpler approach: stick to GET and POST, encoding the intended action in the URL path. Instead of debating HTTP methods, you might use endpoints like:

GET  /api/employee/read/{employee_name}
POST /api/employee/deactivate/{employee_name}
POST /api/employee/archive/{employee_name}
POST /api/employee/remove/{employee_name}

Other Combinations of HTTP Methods

You'll inevitably encounter clients that only support POST requests - especially in enterprise environments. In such a case, embrace a practical solution: move the action into your URL path instead of depending on HTTP methods to convey intent:

# Previously:
GET /api/employee/123

# Now you'll need:
POST /api/employee/123
# Previously:
POST /api/employee/123

# Now you'll need:
POST /api/create-employee/123
# Previously:
DELETE /api/employee/123

# Now you'll need:
POST /api/delete-employee/123

Also, you'll sometimes meet API clients that understand only two actions: GET and POST. Use GET for getting things out of your systems (read-only operations), and then POST for everything else. Design your URLs as above to handle API clients of this sort.

Finally, the HTTP protocol lets you define your own custom HTTP methods, such as "MODIFY" or "REVOKE", but don't do it. Use only standard HTTP methods - don't make life difficult for other people.

More on GET and Server-Side State

GET is unique among HTTP methods because it's designed to be read-only. But let's look at what "read-only" really means in production systems when you send multiple requests asking for the same resource.

When you GET a resource, your monitoring system might log the request, your analytics might track the access pattern, and your audit system might record who accessed what. These operations do modify server state, so maybe you shouldn't use GET after all?

The answer is that you can still use GET because the key distinction is this: GET should never modify the resource state itself or any business-relevant data. System-level operations like logging, monitoring, and auditing are acceptable side effects that don't violate GET's core principle of being safe for resource state.

So the practical rule is simple: if a GET request changes something that a client cares about or could notice - that's a violation. If it only affects behind-the-scenes operational concerns - that's perfectly fine.

Let's talk about the state now.

What Does It Mean That REST APIs Are Stateless?

What does "stateless" mean in REST APIs? Let's understand through a real-world example: a train connection search engine. Consider two approaches to designing this API:

Bad design (stateful)

  • You enter search criteria (Boston to New York, July 19, 2025)
  • The server saves these criteria in a database with a 5-minute expiration
  • The server returns an ID (say, "search123")
  • Future requests just send this ID:
GET /search?id=search123&page=5
  • The server looks up your original criteria using this ID
  • The "state of your journey" through the website is forgotten after 5 minutes

The stateful approach creates several problems:

  • Scaling challenges: adding servers becomes complex - if you add Server B, how does it know about the search state saved on Server A?
  • Hidden dependencies: each request depends on previous server-side state
  • Broken bookmarks: say, you want to bookmark page 5 and return to it later - you can't, because the search ID expires

Good design (stateless)

Every request contains all the needed information:

GET /search?from=Boston&to=NewYork&page=5&departure=2025-07-19

In a stateless design, each request stands alone. It doesn't matter which server handles it or when it arrives - all the information needed is right there in the URL.

This makes the API:

  • Easier to scale (any server can handle any request)
  • More reliable (no expired states)
  • Simpler to maintain (no state management code)
  • Easy to bookmark and share (URLs contain everything)

This illustrates why REST APIs should be stateless: avoid storing request context on the server when you can include it in the request itself.

HTTP Status Codes Useful in Practice

In real-world APIs, these status codes do the heavy lifting:

Status codeDescription
200 OK200 OK is all you need for success. Use it for all successful operations - whether you're returning data, confirming a deletion, or completing any other action. While HTTP defines other 2xx status codes, they often create more confusion than clarity. Stick to 200 OK as your universal success indicator.
400 Bad Request400 Bad Request - client-side errors is used whenever the client's request can't be processed due to their input - whether it's malformed data, invalid parameters, or a non-existent resource. This covers all client errors except security-related issues (that's what 403 is for).
403 Forbidden403 Forbidden - all security issues. Use this status code for any security-related rejection: invalid credentials, missing authentication, or insufficient permissions. While HTTP defines 401 for missing or unknown credentials and 403 for permission issues, real-world APIs are simpler: your clients always know what credentials to use, they just might get them wrong. Standardize on 403 for all auth failures.

When working with clients that can't process HTTP status codes properly, you'll need to always return 200 OK and include the real status in your response payload. This pattern looks like here:

# API client sends:
GET /api/create-employee/richard.roe

# You'll need to return:
HTTP status: 200 OK
Message: {"status":404, "message":"No such employee"}

Status Codes to Avoid in REST APIs

Status codeDescription
404 Not Found
  • 404 Not Found - ambiguous gray area - This status code suffers from dual meaning: when you receive one, is the API endpoint missing or the resource not found?
  • Also, consider distributed systems: should you return 404 when an employee exists in HR but not in Payroll?
  • How about the log analytics system that another team in your organization uses, can it distinguish between "404 as in employee not found" and "404 as in API server is misconfigured"?
  • Consider saving yourself from this complexity: let your API server return 404 automatically for unknown URLs only, and don't actually use it yourself. This creates a clear distinction that API clients can rely on.
  • If you're still planning to use 404, make sure you have answers to this sort of questions prepared well in advance because 404 will create confusion when it's least expected.
  • 404 really is a gray area. You can't be ever sure what it actually means, even though in theory it should be an ideal choice to indicate missing resources. Use with caution and understand that confusion will arise at one point.
500 Internal Server Error500 Internal Server Error - unhandled exceptions only. Let this status code happen naturally when your code throws an unexpected error. Never return it explicitly in your error handling. This convention is deeply ingrained in web development, and deviating from it will only confuse your API clients.
503 Service Unavailable503 Service Unavailable - who sent it really? While 503 seems perfect for indicating temporary system overload or downstream dependencies being down, it has a practical limitation: clients can't tell if the 503 comes from your API or from infrastructure components like load balancers, gateways, or proxies. This ambiguity makes it difficult to handle 503 responses meaningfully.

HTTP status codes like 404 and 503 predate modern REST APIs - they were designed for web servers serving HTML pages, not for APIs exchanging data. While these codes made sense in their original context, they can create ambiguity in API scenarios.

This historical baggage explains why many modern APIs don't use them at all, or use them with extra care, preferring instead to return 200 OK with detailed status information in the response body. This approach gives you more control over error handling and eliminates ambiguity about where the status code originated.

Use Meta Elements in Responses

HTTP status codes break down in practice for several reasons:

  • Many clients ignore HTTP status codes entirely, processing only the response body. During troubleshooting, you often see only the JSON responses in logs, without the corresponding HTTP status.
  • Status codes are too coarse-grained for modern APIs. How do you express "resource found but archived" or "partial data available"? The standard HTTP codes don't cover these nuances.
  • They don't distinguish between different types of errors. Take 403 Forbidden - did the API client provide invalid credentials, or does the user lack permissions? The status code alone can't tell you.

This is why robust APIs include a "meta" object in JSON responses to provide richer status information. Beyond basic HTTP status codes, this lets you communicate specific error conditions and helpful messages.

This is required because complex business systems create status code ambiguities that HTTP alone can't resolve:

For instance:

{
    "meta": {
        "status": 200,  // Same as the HTTP status
        "is_ok": false, // The actual status of the operation
        "error_code": "HR_NOT_FOUND",
        "message": "Employee exists in Payroll but not in HR"
    }
}

This helps to answer some nagging questions:

  • If you return 200 OK but is_ok: false, you're sending mixed signals. The employee exists somewhere, but not everywhere - is this a success?
  • If you return 400 Bad Request, you're forcing API clients to understand your internal systems. Should they be blamed (400 Bad Request) for not knowing about HR vs Payroll distinctions?

In these situations, you often need to balance competing concerns. For instance, in our example, using 200 OK with "is_ok: false" prioritizes encapsulation:

  • The HTTP status code 200 tells clients "your request was processed correctly at the technical level"
  • The meta field is_ok: false tells them "but the business operation had limitations"
  • We consciously avoid 400 Bad Request because:

    • The client's request was perfectly valid - they asked for an employee data
    • Our API successfully processed their request
    • The fact that data exists in some systems but not others is our internal complexity
  • This separation achieves two goals:

    • Technical status (HTTP 200) - "the API worked as designed"
    • Business status (meta object) - "here's what happened in business terms"

Standardize on a clear meta response structure that can express these nuances. Keep your HTTP status codes simple (stick to 200, 400, 403, 503), and put the detailed business logic in your meta object. Don't venture into obscure HTTP status codes - they'll only add to the confusion.

Keeping detailed status information in your response's meta object gives your APIs one unexpected advantage - it increases the flexibility and portability of your APIs.

While your REST API uses HTTP status codes today, you might need to expose the same functionality over message queues (AMQP) or other protocols tomorrow, so this is something to keep in mind too.

Both Humans and Applications Like Clean URLs

In REST, clean URLs are API endpoint addresses that humans can easily read and understand without documentation.

They follow logical patterns and clearly indicate what resource you're working with. For example, consider these two URLs for an employee API:

Good:
/api/employee/123/assignments

Bad:
/api/proc.aspx?t=emp&id=john.doe&act=asgn

What makes a URL clean:

  • Uses nouns representing resources (employees, assignments)
  • Forms logical hierarchies (assignments belong to an employee)
  • Uses natural plural/singular forms
  • Avoids technical details (no .aspx, no database table abbreviations)
  • Uses consistent conventions throughout the API

Some examples used by RESTful applications:

GET /api/departments/sales/employees  # Get all employees in Sales
GET /api/employee/123/projects/active # Get active projects for employee john.doe
GET /api/teams/engineering/managers   # Get Engineering team managers

Use Correlation IDs in Responses

Correlation IDs help track requests across distributed systems. Think of them as package tracking numbers - they let you, and your API clients, trace a single request's journey through multiple services, logs, and databases.

For example, when your API returns:

{
    "meta": {
        "cid": "3080a00bb34f4c7ebcb3e22f9cc41bd6", // Correlation ID
        "timestamp": "2025-07-19T14:30:00Z"        // Response timestamp on server
    },
    "data": {
        "employee_no": 123,
        "name": "John Doe"
    }
}

This ID becomes invaluable when:

  • Troubleshooting issues across multiple systems
  • Tracking a request through load balancers and API services
  • Matching backend errors with specific client requests
  • Supporting customers reporting problems ("Please reference ID 3080a00bb34f4c7ebcb3e22f9cc41bd6")
  • Analyzing performance across your service chain

In Zato, each time your service is invoked, it will have access to the current correlation ID via self.cid.

Best practices:

  • Pass them through every system that handles the request
  • Include them in all logs
  • Always return them in API responses
  • Make them easily findable in your logging system

This simple addition makes production support significantly easier, especially in complex distributed systems.

Creating REST APIs in Python

With all the background information in mind, it's time for some Python programming now.

Here's how a complete CRUD set of operations will look like. You can copy-paste it into the same "api.py" file as previously. And by the way, "api.py" is just a name, you can as well call it "employee.py" or similar.

Create it in your IDE, and read the discussion that follows.

# -*- coding: utf-8 -*-

# stdlib
from dataclasses import dataclass

# Zato
from zato.common.typing_ import strdict
from zato.server.service import Model, Service

# ###################################################################################

@dataclass(init=False)
class Meta(Model):
    cid: 'str'
    is_ok: 'bool'
    timestamp: 'str'

# ###################################################################################

@dataclass(init=False)
class GetRequest(Model):
    employee_no: 'int'

@dataclass(init=False)
class GetResponseData(Model):
    name: 'str'
    department: 'str'

@dataclass(init=False)
class GetResponse(Model):
    meta: 'Meta'
    data: 'GetResponseData'

# ###################################################################################

@dataclass(init=False)
class CreateRequest(Model):
    name: 'str'
    department: 'str'

@dataclass(init=False)
class CreateResponseData(Model):
    is_success: 'int'

@dataclass(init=False)
class CreateResponse(Model):
    meta: 'Meta'
    data: 'CreateResponseData'

# ###################################################################################

@dataclass(init=False)
class UpdateRequest(Model):
    employee_no: 'int'
    name: 'str | None'
    department: 'str | None'

@dataclass(init=False)
class UpdateResponse(Model):
    meta: 'Meta'

# ###################################################################################

@dataclass(init=False)
class DeleteRequest(Model):
    employee_no: 'str'

@dataclass(init=False)
class DeleteResponse(Model):
    meta: 'Meta'

# ###################################################################################

class EmployeeService(Service):
    """ Base Python class for all employee-related services.
    """

    def get_meta(self, is_ok:'bool') -> 'Meta':

        # Build the model ..
        meta = Meta()

        # .. fill it out ..
        meta.cid = self.cid
        meta.is_ok = is_ok
        meta.timestamp = self.time.utcnow()

        # .. and return it to our caller.
        return meta

# ###################################################################################

class GetEmployee(EmployeeService):

    input  = GetRequest
    output = GetResponse

    def get_data(self, employee_no:'int') -> 'strdict':

        # Log what we're doing ..
        self.logger.info(f'cid:{self.cid} -> Returning employee data for {employee_no}')

        # .. and return the data to our caller.
        return {'EmployeeName':'John Doe', 'EmployeeDept':'HR'}

    def handle(self):

        # Add type hints
        input:'GetRequest' = self.request.input

        # Invoke our data source to obtain the employee data ..
        employee_data = self.get_data(input.employee_no)

        # .. prepare our business response ..
        response_data = GetResponseData()

        # .. map employee data to our model ..
        response_data.name = employee_data['EmployeeName']
        response_data.department = employee_data['EmployeeDept']

        # .. use True in is_ok to indicate that there were no errors ..
        meta = self.get_meta(True)

        # .. build our response ..
        response = GetResponse()

        # .. attach both meta and business data ..
        response.meta = meta
        response.data = response_data

        # .. and return everything to our API client.
        self.response.payload = response

# ###################################################################################

class CreateEmployee(EmployeeService):

    input  = CreateRequest
    output = CreateResponse

    def save_data(self, data:'CreateRequest') -> 'int':

        # Log what we're doing ..
        self.logger.info(f'cid:{self.cid} -> Adding employee: {data}')

        # This is the place in which your service
        # will communicate with a database to save the data ..

        # .. in this tutorial service, we're simply always returning the same ID ..
        employee_id = 123

        # .. we can return it now.
        return employee_id

    def handle(self):

        # Add type hints
        input:'GetRequest' = self.request.input

        # Invoke our data source to save the employee data ..
        employee_no = self.save_data(input)

        # .. prepare our business response ..
        response_data = CreateResponseData()
        response_data.employee_no = employee_no

        # .. use True in is_ok to indicate that there were no errors ..
        meta = self.get_meta(True)

        # .. build our response ..
        response = CreateResponse()

        # .. attach both meta and business data ..
        response.meta = meta
        response.data = response_data

        # .. and return everything to our API client.
        self.response.payload = response

# ###################################################################################

class UpdateEmployee(EmployeeService):

    input  = UpdateRequest
    output = UpdateResponse

    def save_data(self, data:'UpdateRequest') -> 'None':

        # Log what we're doing ..
        self.logger.info(f'cid:{self.cid} -> Updating employee: {data}')

        # This is the place in which your service
        # will communicate with a database to save the data.

    def handle(self):

        # Add type hints
        input:'UpdateRequest' = self.request.input

        # Invoke our data source to save the employee data ..
        self.save_data(input)

        # .. use True in is_ok to indicate that there were no errors ..
        meta = self.get_meta(True)

        # .. build our response ..
        response = UpdateResponse()

        # .. attach the meta object ..
        response.meta = meta

        # .. and return everything to our API client.
        self.response.payload = response

# ###################################################################################

class DeleteEmployee(EmployeeService):

    input  = DeleteRequest
    output = DeleteResponse

    def delete(self, data:'DeleteRequest') -> 'None':

        # Log what we're doing ..
        self.logger.info(f'cid:{self.cid} -> Deleting employee: {data}')

        # This is the place in which your service
        # will communicate with a database to delete the record.

    def handle(self):

        # Add type hints
        input:'DeleteRequest' = self.request.input

        # Invoke our data source to save the employee data ..
        self.delete(input)

        # .. use True in is_ok to indicate that there were no errors ..
        meta = self.get_meta(True)

        # .. build our response ..
        response = DeleteResponse()

        # .. attach the meta object ..
        response.meta = meta

        # .. and return everything to our API client.
        self.response.payload = response

# ###################################################################################
  • This REST implementation is an employee management API with basic operations (get, create, update, delete) for employee records.

  • What you see is a template for building REST APIs with consistent structure and error handling.

  • Each API operation is a regular Python class whose "handle" method handles incoming requests.

  • Because it's plain Python code, we can move the common functionality to a base class called "EmployeeService".

  • Each API operation has two main parts:

    • Request model: Defines what data clients must send
    • Response model: Defines what data the API will return
  • Every API response includes two sections:

    • Meta: Contains technical details like correlation ID (cid), success status (is_ok), and timestamp
    • Data: Contains the actual business information (like employee details)
  • Each operation (get, create, update, delete) follows the same pattern:

    • Define what data comes in
    • Define what data goes out
    • Process the request
    • Return a structured response
  • The code emphasizes good practices:

    • Type hints for better code safety
    • Consistent logging for debugging
    • Clear separation between data models and business logic
    • Standardized reporting through the meta object
  • UpdateRequest demonstrates how Zato simplifies parameter handling in your API code:

    • The platform unifies access to all parameters - whether they're URL path variables like "employee_no" or JSON request body fields like "name" and "department" - through a single interface: "self.request.input".
    • This decouples your business logic from request parsing details, making it easier to refactor parameter sources without touching service code.
  • Every line of code in your API services should tell a story through its comments. Think of these comments as signposts guiding future developers on their journey through your codebase. Since APIs often live for years or even decades, clear comments help maintainers understand your code's intent without having to decode each line. This documentation isn't just good practice - it's essential for long-term maintainability.

Adding REST Endpoints to Your Interface

In your Zato dashboard at http://localhost:8183, go to Connections → Channels → REST and create a new REST channel (endpoint) for each of the services.

Choose "No security definition" for each of the channels - we'll get to that in a moment.

You should have a total of 4 channels for each of the CRUD operations:

Create

Read

Update

Delete

Sending Requests to REST APIs with Postman

We can now use Postman to invoke our API endpoints on localhost:17010. Remember, localhost:8183 is where your dashboard is, but the actual API server is on port 17010.

Let's check JSON responses from two of them:

Get Employee

Create Employee

REST API Security and Authentication

So far, none of the API calls required any credentials, and we need to change that.

To control access to our API endpoints, we need to add a security mechanism, and we have two options: HTTP Basic Auth or API keys.

While both HTTP Basic Auth and API keys provide robust security, Basic Auth is the recommended starting point. Its widespread support means better integration with development tools, testing frameworks, and client libraries.

In Zato, create a new HTTP Basic Auth Security for each API client that will be invoking your endpoints. For instance, your HR, Payroll or Onboarding systems will have their own credentials that won't be shared with any other.

Here, we're creating API credentials for HR and attach it to a REST channel (endpoint).

From now on, to access this endpoint, the correct username and password will have to be entered.

And by the way, what if you'd like to attach multiple security definitions to a channel? We're not going to show it here but to achieve that, use security groups - create a group, add individual API security definitions to it, and then attach a group to a channel.

Choosing Which HTTP Methods Clients Can Use

By default, Zato endpoints accept any HTTP method. This deliberate choice maximizes compatibility with API clients. Remember how some clients can only use POST, even for read or delete operations? This default configuration lets them work without modification.

However, if you need to restrict allowed methods, you can specify them in your channel configuration:

Returning Status Codes and HTTP Headers

200 OK is what Zato returns by default unless you explicitly indicate a different status code. Similarly, it will only populate the "Content-Type" header, leaving it up to you to return other headers. So let's see how you can do both now.

# -*- coding: utf-8 -*-

# stdlib
from http.client import BAD_REQUEST

# Zato
from zato.server.service import Service

class MyService(Service):
    def handle(self):

        # Your implementation goes here
        pass

        # We want to send 400 Bad Request in the response
        self.response.status_code = BAD_REQUEST

        # We want to send these two custom headers in the response
        self.response.headers['X-My-Header-1'] = 'Hello'
        self.response.headers['X-My-Header-2'] = 'World'

Accessing Request Headers and Other Metadata

At times, you'll need to check what API clients sent in via means other than JSON requests - HTTP headers are the most commonly used vessel, but it can be form data as well. Here's what you do then.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class MyService(Service):
    def handle(self):

        # GET, POST, DELETE etc.
        self.request.http.method

        # A dictionary of GET parameters
        self.request.http.GET

        # A dictionary of POST parameters
        self.request.http.POST

        # URL path of the request, e.g. /api/employee/create
        self.request.http.path

        #
        # A dictionary that combines GET parameters with URL path parameters
        # so that you can have one convenient place to access both regardless
        # of what the API client uses (because it doesn't matter).
        #
        # Example:
        #
        #  GET params are ?department=HR&unit=ABC
        #  Path params are /api/employee/get/123
        #  Now, self.request.http.params will be:
        #  {'department':'HR', 'unit':'ABC', 'employee_no':123}
        #
        self.request.http.params

        #
        # Some API clients, notably some WebHooks, will send requests to you using
        # HTTP form data, rather than JSON. In such a case, set the data format
        # to "Form" in your channel's definition, and then access the form data as below.
        #
        request = self.request.http.get_form_data()

REST API Testing

Zato comes with its own API testing tool that lets you write API tests in plain English without programming knowledge, although you can always extend it in Python yourself.

Instead of writing code, you describe your test steps using natural language commands that specify which endpoints to call, what data to send, and what responses to expect.

Each test consists of clear, readable steps that outline the test scenario. You define the API address, URL path, HTTP method and any request data, then specify what should happen when the endpoint is invoked.

The tool validates responses against your expectations, checking status codes, response fields, and data types - all described in plain English commands.

It all resembles a unit-test, just one written in a natural language.

Feature: Employee API

Scenario: *** Get Employee API ***

    Given address "http://localhost:17010"
    Given URL path "/api/employee/get/123"
    Given REST method "GET"

    When the URL is invoked

    Then status is "200"
    And path "/meta/is_ok" is True
    And path "/meta/cid" is not empty
    And path "/meta/timestamp" is not empty
    And path "/data/name" is "John Doe"
    And path "/data/department" is "HR"

What About Non-REST APIs?

At the end of the day, REST is just one chapter in the long history of application interfaces, and APIs existed long before REST and HTTP. While REST is the dominant approach today, it's just one way to expose application functionality.

Here are two other common API styles you'll encounter:

  • SOAP: legacy but still around. Still used in enterprise systems, the Simple Object Access Protocol is an architectural style that's somewhat more complex than REST. You'll encounter SOAP APIs in older systems - especially in financial environments - so knowing how to work with them is valuable.

    However, for new projects, REST is the clear choice due to its simplicity and broad adoption. Understand how to invoke SOAP, but simply ignore it in new projects, and try to use REST whenever possible instead.

  • WebSockets: bidirectional communication. Two key advantages make WebSockets worth considering:

    1. Simplified Network Setup: The client initiates the connection, but once established, the server can push data to the client. This eliminates the need for complex firewall configurations that would otherwise be needed for server-to-client communication.
    2. Reduced Overhead: Without HTTP headers in every exchange, WebSockets can be more efficient for frequent, small-payload communications. This is particularly valuable for IoT devices where every byte matters - why waste processing power on kilobyte-sized headers when your actual payload is just 500 bytes?
    Yet, WebSockets come with a significant trade-off: they're stateful. Unlike REST's stateless design, WebSocket-based TCP connections must be maintained and managed, adding complexity to your server architecture.

So yes, while APIs can be, and have been built, using numerous other technologies (FTP, MQ, SQL procedures, and many others), REST should be your default choice for new projects unless you have specific requirements that demand otherwise.

What We've Learned

REST APIs form the backbone of modern system integration, but real-world implementations often differ from theoretical models. Let's recap the key practical insights from this tutorial:

Focus on Simplicity

  • Stick to GET and POST when in doubt or when forced to
  • Use 200, 400, and 403 as your primary status codes
  • Keep URLs clean and readable
  • Include detailed status in response meta objects

Think About Scale

  • Design stateless APIs from the start
  • Return meta-information in addition to the actual business information
  • Structure responses consistently

Stay Practical

  • Not every client supports all HTTP methods
  • Internal complexity belongs in your implementation, not your API
  • Meta objects help port APIs across different protocols
  • Inline comments and testing are crucial - APIs live for years

Remember Business Reality

  • REST is a means to an end, not a goal itself
  • Perfect theoretical compliance matters less than robust, working solutions that continue to work reliably for many years
  • Focus on solving business problems, not debating HTTP semantics
  • Be prepared to handle edge cases gracefully

By following these principles, you'll build APIs that are easy to use, simple to maintain, and ready for production. The goal isn't to create perfectly RESTful services - it's to create reliable interfaces that solve real business problems.

Next steps? Start building. The best way to learn is through practice, and every API you create will teach you something new about practical system integration.

Continue Your API Learning Journey