Backend Architecture
Loose coupling, separation of concerns and layered architecture should not only be applied to frontend development. These principles can also be applied during backend development. For example, concepts such as route navigation, data access, data processing and data models can be separated and tested in isolation.
Project structure
Putting the backend in the same repository as the frontend allows developers to easily import data models from the backend. Within the backend directory, developers should consider separating the following elements into dedicated directories:
- Middleware providers
- Routes
- Data access
- Data models
- Tests
While providers, routes, and tests, can live in the root backend project, consider putting data models and data access into their own dedicated package(s). Ideally, these layers should be able to exist independently from the rest of the app.
Directoryapi/
Directoryroutes/
Directoryapi/
Directoryv1/
Directorytodos/
- β¦
- _middleware.dart
Directorytest/
Directoryroutes/
Directoryapi/
Directoryv1/
Directorytodos/
- β¦
Directorypackages/
Directorymodels/
Directorylib/
Directorysrc/
Directoryendpoint_models/
- β¦
Directoryshared_models/
- β¦
Directorytest/
Directorysrc/
Directoryendpoint_models/
- β¦
Directoryshared_models/
- β¦
Directorydata_source/
Directorylib/
Directorysrc/
- β¦
Directorytest/
Directorysrc/
- β¦
Models
A models
package should clearly define the necessary data models that the backend will be sharing with the frontend. Defining endpoint models makes the data necessary to communicate between frontend and backend more explicit. It also creates a data structure that can communicate additional metadata about content received, such as the total count of items and pagination information.
Data Access
A data source package should allow developers to fetch data from different sources. Similar to the data layer on the frontend, this package should abstract the work of fetching data and providing it to the API routes. This allows for easy development by mocking data in an in-memory source when necessary, or also creating different data sources for different environments.
The best way to achieve this is by making an abstract data source with the necessary CRUD methods, and implementing this data source as needed based on where the data is coming from.
Routes
Routes should follow common best practices for REST API design.
Endpoints Should Have Descriptive Paths
Endpoints should be named for the collection of objects that they provide access to. Use plural nouns to specify the collection, not the individual entity.
Nested paths then provide specific data about an individual object within the collection.
When setting up a collection of objects that is nested under another collection, the endpoint path should reflect the relationship.
Use Query Parameters to Filter Properties of GET results
Query parameters serve as the standard way of filtering the results of a GET request.
Map the Request Body of POST, PUT, and PATCH Requests
On requests to the server to create or update items, endpoints should take a stringified JSON body and decode it into a map so that the appropriate entity in the data source is changed. Since this is a common process for all create/update requests, consider adding a utility to map the request body.
The request body can then be converted into the correct data model like in the endpoint code.
Use PATCH to Send Update Requests
For update requests, PATCH is more advisable than PUT because PATCH requests the server to update an existing entity, while PUT requests the entity to be replaced.
DELETE should only require an identifier
DELETE requests should require nothing more than an identifier of the object to be deleted. There is no need to send the entire object.
Return Appropriate Status Codes
Routes should also return proper status codes to the frontend based on the results of their operations. When an error occurs, sending a useful status and response to the client makes it clear what happened and allows the client to handle errors more smoothly.