Week 2 | Lesson 5

Application Architecture

APIs
Application Architecture

Application Programming Interface

API

What is API

API stands for Application Programming Interface.

Most commonly, when we think of an API, we think of a web API such as REST API or GraphQL.

However anytime any program or application communicates with another program or application, it is using some kind of API. It can be command line parameters, function calls, or network requests or hardware APIs.

  • It is a set of rules and protocols that allow different software applications to communicate with each other.
  • In contrast to user interface, API is meant for program to program or computer to computer communication.
  • There are many forms of APIs, such as web APIs, library APIs, and operating system APIs. Some APIs are specific to a particular programming language, some are specific to a particular application.

Design of API

When you think about designing an interface, you first need to think about the problem you are trying to solve with it. Consider how the API will be used, and design it in a way that is convenient for the users of the API.

This is why in this course, we will start by designing and interface (API) and only then we will implement the service and data model behind it.

If we did it the other way around, we would be tempted to design the service and data model in a way that is convenient for us, the developers, and not in a way that is convenient for the users of the API.

What API standards are there?

REST is based on HTTP protocol and uses standard HTTP methods to interact with the server. It is widely used in web applications and is supported by most programming languages.

GraphQL is a query language and runtime for APIs that allows clients to request exactly the data they need. Instead of multiple REST endpoints, a single GraphQL endpoint can serve many queries or mutations. It is efficient for frontend-driven applications and can reduce over-fetching and under-fetching of data.

Websockets WebSockets enable two-way, persistent communication between client and server over a single TCP connection. Unlike REST, which is request-response, WebSockets allow servers to push data to clients without polling.

REST API

What is REST

REST stands for Representational State Transfer.
It is an architectural style for designing networked applications. Systems that follow REST principles are often called RESTful systems. Characteristics of RESTful systems include statelessness, client-server architecture, and a uniform interface.
  • Statelessness
    Each request from a client to a server must contain all of the information necessary process the request, without relying on any server state being held between requests.
  • Client-server architecture
    The client and server are separate and independent of each other, only communicating by well-defined requests and responses. This allows each to be developed and scaled independently.

    In real applications, client is usually responsible for the user interactions and the server is responsible for the data storage and processing.
  • Uniform interface
    The API should be designed in a way that is consistent, predictable, handles errors gracefully, is platform-agnostic, and is easy to understand and use.

REST API

Communication through REST API is done using standard HTTP methods, such as GET, POST, PUT, and DELETE.

REST communication is request-response protocol, which means that the client sends a request to the server, and the server sends a response back to the client.

Each request is sent to a unique URI (Uniform Resource Identifier), which represents a resource on the server.

The server processes the request and sends back a response, which may include data, status, and other information, usually in JSON or XML format.

Request

  • HTTP Method
    Defines the type of action to be performed on the resource.
    GET /accounts
  • URI
    Identifies a unique resource on the server. It is usually composed of path and optionally query parameters.
    GET /accounts/123/users?limit=10&search=joe
  • Headers
    These can be used to send additional data with the request, such as the content type or an authorization token.
    Content-Type: application/json
    Authorization: Bearer some-token-value
  • Body
    Body is usually sent only with POST, PUT and PATCH requests. In most cases, this will be formatted as JSON or XML.
    { "username": "testUser", "password" : "123456" }

Response

  • HTTP Status Code
    A numerical code that indicates the success or failure of the request. There is a convention for what status code should be used in what situation.
  • Headers
    As in the request, headers in the response can be used to pass additional information. This might include the content type of the response, or a Set-Cookie header to store information in the client's browser.
  • Body
    This contains the actual data being returned from the server. This will usually be in JSON or XML format, or could also be plain text.
    { "id": 1, "username": "testUser", "email": "testUser@example.com" }

Methods

In theory, you can use all methods of HTTP protocol to communicate with REST API.

In practice, you will mostly use ...

  • GET
    Used to retrieve data from the server. It should never change the state of the server.
  • POST
    Used to send data to the server to create a new resource.
  • PUT
    Used to send data to the server to update an existing resource. Changes should be idempotent, meaning that if you send the same request multiple times, the result should be the same as if you sent it once. In other words, PUT should be used to update the resource as a whole.
  • DELETE
    Used to delete a resource from the server.
  • PATCH
    Used to partially update a resource on the server.

Paths

URI is the path to the resource on the server.

You can use path parameters to specify a particular resource, and query parameters to, for example, filter or paginate the results.

Here is the conventional structure of the resource:

/resources/ {path-parameter} /sub-resource ? param1=value & param2=value

  • resource path
  • path parameter
  • path and query parameter separator
  • query parameter separator
  • query parameters and their values

Headers

Headers are used to pass additional information with the request or response.

Headers are usually conventional, meaning that there are some standard headers that are used in most APIs. REST services can also define their own custom headers.

Some of the most common conventional headers are:

  • Content-Type
    Used to specify the format of the body of the request.
    Content-Type: application/json
  • Accept
    Used to specify the format of the response.
    Accept: application/json
  • Authorization
    Used to pass an authorization token with the request.
    Authorization: Bearer some-token-value
  • X-Api-Key
    Used to pass an API key with the request.
    X-Api-Key: some-api-key-value

Body

Body is usually sent only with POST, PUT and PATCH requests.

Body is almost exclusively custom, meaning that it is up to the service to define what the body of the request or response should look like.

Most common formats for the body are JSON and XML.

JSON:

						
							{
								"id": 1234,
								"firstName": "Monika",
								"lastname": "Protivova",
								"email": "monika.protivova@gmail.com",
								"isAdmin": true
							}
						
					

XML:

						
							<user>
								<id>1234</id>
								<firstName>Monika</firstName>
								<lastname>Protivova</lastname>
								<email>monika.protivova@gmail.com</email>
								<isAdmin>true</isAdmin>
							</user>
						
					

Status Codes

HTTPS status codes are used to indicate the success or failure of the request.
  • 1xx
    Informational responses. The request was received, continuing process.
    • 100 - Continue
    • 101 - Switching Protocols
  • 2xx
    Success. The action was successfully received, understood, and accepted.
    • 200 - OK
    • 201 - Created
  • 3xx
    Redirection. Further action must be taken in order to complete the request.
    • 301 - Moved Permanently
  • 4xx
    Client Error. The request contains bad syntax or cannot be fulfilled.
    • 400 - Bad Request
    • 401 - Unauthorized
    • 403 - Forbidden
    • 404 - Not Found
  • 5xx
    Server Error. The server failed to fulfill an apparently valid request.
    • 500 - Internal Server Error

REST API design practices

When designing a REST API, it is important to follow some good practices.

Naming conventions

  • Paths should be nouns and should be plural.
  • Paths should represent hierarchical relationships.
  • Paths should preferably be lowercase, and not case-sensitive.
  • Query parameters should be named in consistent matter, either using
    • camelCase
    • snake_case
    • kebab-case
    • ... but not combining them

REST API design practices

When designing a REST API, it is important to follow some good practices.

Versioning

  • Once you publish a (public) API, you may not be able to control who uses it.
  • API versioning is thus important to ensure that any changes are backward compatible.

REST API design practices

When designing a REST API, it is important to follow some good practices.

Request and response format

  • API should return data in a consistent format, such as JSON or XML.
  • Naming conventions should be consistent, for example, use camelCase or snake_case consistently throughout the API.
  • For services written in Java, it is more nature to use camelCase, because it matches class field naming conventions.

REST API design practices

When designing a REST API, it is important to follow some good practices.

Response codes and error handling

  • Each method should respond with appropriate status code.
  • For example:
    • successful requests that do not modify resource should be either 200 or 204
    • successful requests that create a new resource should be 201
    • requests that fail due to user error should return 4xx
    • requests that fail due to user input error should fail with 400
    • requests tha fail due to authentication error should fail with 401
    • requests that fail due to authorization error should fail with 403
    • requests that fail due to resource not found should fail with 404
    • requests that fail due to server error should return 5xx

REST API design practices

When designing a REST API, it is important to follow some good practices.

Overall consistency

  • API should be consistent in its design and behavior.
  • For example, if POST method for one resource returns the newly created object, then POST methods for similar resources should behave consistently — either returning the created object or a 201 Created response with a Location header.

Application Architecture

Overview

Application Architecture

Ktor doesn’t impose any specific structure, but a well-organized application will typically follow a layered architecture.
  1. Routing Layer
    This is where you define the routing and handle incoming requests and return responses.

  2. Service Layer
    Contains the business logic of your application.

  3. Data Layer
    Responsible for data access, such as database interactions or external API calls.

  4. Domain and Models
    Contains data objects which represent the business entities in your application.

  5. Configuration
    Contains the configuration of your application, such as environment variables, logging, and other settings.
    It also contains database connection settings, dependency injection configuration, security configuration, etc.

We will cover each of these layers in detail in the following lessons.

Application Architecture

Why do we need to structure our application?

Structuring your application is important for several reasons:

  • Separation of concerns
    It helps to separate concerns (responsibilities). Each layer has its own responsibility exposed through its API. This in practice also prevents leaking of implementation details between layers, only exposing the necessary interfaces.

  • Modularity and Reusability
    It makes the application more maintainable and scalable, as you can easily add new features or modify existing ones without affecting other parts of the application.

  • Testability
    It makes it easier to test the application, as you can test each layer separately.

Layered Architecture Patterns

Layered Architecture
								
									src/
									├── routes/
									│	├── OrderRoutes.kt
									│	└── CustomerRoutes.kt
									├── services/
									│	├── OrderService.kt
									│	└── CustomerService.kt
									├── repositories/
									│	├── OrderRepository.kt
									│	└── CustomerRepository.kt
									├── models/
									│	├── Order.kt
									│	└── Customer.kt
									├── configuration/
									└── Application.kt
								
							
Pros
  • Simple and easy to understand.
  • OK for small applications.


Cons
  • Not suitable for large applications.
  • Can lead to tight coupling between layers.
  • Can lead to unwanted dependencies between domains.
Domain-Driven Design (DDD)
								
									src/
									├── order/
									│   ├── OrderRoutes.kt
									│   ├── OrderService.kt
									│   ├── OrderRepository.kt
									│   ├── Order.kt
									│   └── OrderDto.kt
									├── customer/
									│   ├── CustomerRoutes.kt
									│   ├── CustomerService.kt
									│   ├── CustomerRepository.kt
									│   ├── Customer.kt
									│   └── CustomerDto.kt
									├── common/
									│   ├── db/
									│   ├── configuration/
									│   └── utils/
									└── Application.kt
								
							
Pros
  • Better separation of concerns.
  • Each domain can have its own routes, services, repositories, and models.
  • Easy to add new domains without affecting existing ones.
  • Easy to separate domains into separate modules or microservices.
  • More modular and easier to maintain.


Cons
  • Can lead to duplication of code if not managed properly.
  • Can be more complex to set up and maintain.

Layered Architecture Patterns

You can also combine these two patterns to create a more modular and maintainable application, taking advantage of the strengths of both approaches.

						
							src/
							├── customer/
							│	├── routes/
							│	│   ├── CustomerRoutes.kt
							│	│	├── CreateCustomerRequest.kt
							│	│	└── CustomerResponse.kt
							│	├── services/
							│	│   └── CustomerService.kt
							│	├── repositories/
							│	│   ├── CustomerRepository.kt
							│	│	└── CustomerDto.kt
							│	└── models/
							│		└── Customer.kt
							├── common/
							│   ├── db/
							│   ├── configuration/
							│   └── utils/
							└── Application.kt
						
					

Routing Layer

Main responsibility of the routing layer is to handle incoming requests and return responses.

The routing layer is responsible for defining the routes of the application, which are the paths that the application responds to.

It's responsibilities include:

  • Defining the routes of the application, which are the paths that the application responds to.
  • Handling incoming requests and returning responses.
  • Mapping HTTP methods to specific actions, such as GET, POST, PUT, DELETE, etc.
  • Validating input data and ensuring it meets the requirements of the business logic.
  • Handling errors and exceptions that may occur during the request processing by mapping them to appropriate HTTP status codes.

Service Layer

Main responsibility of the service layer is to handle the business logic of the application.

Business logic is the part of the application that defines how the application behaves and what it does under certain conditions.

The service layer is often responsible for:

  • Executing the business logic of the application, enforcing business rules and constraints, such as ensuring valid state transitions, data integrity, and consistency.
  • Validating input data and ensuring it meets the requirements of the business logic.
  • Coordinating interactions between different components of the application, such as the data access layer, external APIs, and other services.
  • Mapping data between different representations, such as converting database entities to domain models or DTOs (Data Transfer Objects).
  • Performing complex calculations or data transformations that are part of the business logic.
  • Managing transactions and ensuring that changes to the data are consistent and atomic.
  • Handling errors and exceptions that may occur during the execution of the business logic.

Data Access Layer

Main responsibility of the data access layer is to handle the data storage and retrieval.

This is typically done using a database, but it can also be done using external APIs or other data sources.

This layer is responsible for:

  • Interacting with the database or other data sources to perform CRUD (Create, Read, Update, Delete) operations.
  • Mapping data between different representations, such as converting domain models to database entities or DTOs.
  • Handling database connections and transactions, ensuring that changes to the data are consistent and atomic.
  • Implementing data access patterns, such as repositories or DAOs (Data Access Objects), to encapsulate data access logic.
  • Handling errors and exceptions that may occur during data access operations.

Domain and Models

Domain and models are the data objects which represent the business entities in your application.

They are used to transfer data between different layers of the application.

Domain objects are usually defined as data classes in Kotlin, and they represent the business entities in your application, such as users, accounts, products, etc.

Models are usually defined as data transfer objects (DTOs) in Kotlin, and they represent the data transferred between different layers of the application.

Application Configuration

Configuration is an important part of any application, as it allows you to define the settings and parameters that control the behavior of the application.

We can distinguish between two types of configuration (unofficially):

  • Static
    Configuration that is defined at compile time, such as the application name, version, dependency injection settings, serialization settings, and other settings that do not change during the runtime of the application.

  • Dynamic
    configuration that can change during the runtime of the application, such as environment variables, application secrets, database connection settings, logging settings, and other settings which are environment-specific, sensitive, or can change during the runtime of the application.
If you remember the 12-Factor App methodology, configuration is supposed to be stored in environment variables or configuration files and not hard-coded in the application.

Application

using Ktor

What is Ktor

Ktor is a framework for building asynchronous servers and clients in connected systems using the Kotlin programming language.

It is designed to be lightweight, flexible, and easy to use. It provides a set of tools for building web applications, REST APIs, and microservices.

Ktor is built on top of the Kotlin Coroutines library, which allows you to write asynchronous code in a synchronous style.

We will briefly look at Kotlin Coroutines in the next lesson, for now, don't worry about understanding them.

As any developer, you would start by looking at Ktor documentation.

Application Main

Every application needs an entry point, which is usually a main function.

In Ktor, you can create an application by calling the embeddedServer function, passing it the Netty engine and a lambda with the application configuration.

						
							import io.ktor.server.engine.*
							import io.ktor.server.netty.*

							fun main() {
								embeddedServer(Netty, port = 8080) {
									// Application configuration goes here
								}.start(wait = true)
							}
						
					

Simple Application

Here is a simple application example with json serialization and simple routing.
						
							fun main() {

								val helloService = HelloService(HelloRepository())

								embeddedServer(Netty, port = 8080, host = "0.0.0.0") {

									install(ContentNegotiation) {
										json(Json {
											prettyPrint = true
											isLenient = true
										})
									}

									routing {

										route("/api") {

											route("/hello") {

												get {
													val name = call.request.queryParameters["name"] ?: "Student"
													val locale = call.request.queryParameters["locale"] ?: "en"
													val localizedHello = helloService.getLocalizedHello(name, locale)
													call.respond(localizedHello)
												}

												post {
													val request = call.receive<HelloRequest>()
													val localizedHello = helloService.getLocalizedHello(request.name, request.locale)
													call.respond(localizedHello)
												}
											}
										}
									}
								}.start(wait = true)
							}

						
					

Questions?

Problems?