An Actix 3 server template. Originally based on Actix 2.0 REST server, but has been refactored, updated to Actix 3 and is with extra features.
Add some extra security on top of the Actix 2.0 REST server example and build a wizard that would allow to easily set up a Actix server.
- Actix 3.x HTTP Server
- Multi-Database Support (MySQL)
- JWT Support
- Async Caching Layer with a Simple API
- Public and Secure Static File Service
- Diesel Database Operations are Non-Blocking
- Filesystem Organized for Scale
- .env for Local Development
- Integrated Application State with a Simple API
- Lazy Static Config struct
- Built-in Healthcheck (includes cargo version info)
- Listeners configured for TDD
- Custom Errors and HTTP Payload/Json Validation
- Secure Argon2i Password Hashing
- CORS Support
- Unit and Integration Tests
- Test Coverage Reports
- Dockerfile for Running the Server in a Container
- TravisCI Integration
- Force connection to TSL (HTTPS).
- No JWT based session handling. Sessions use secure cookies that are also stored in background.
- Generate random salt per user password (pull request).
- Refactoring to change to folder structure.
- Rate limiting.
Argon2i: Argon2i Password Hasningactix-cors: CORS Supportactix-identity: User Authenticationactix-redisandredis-async: Async Caching Layeractix-web: Actix Web Serveractix-ratelimit: Rate limitingactix-session: Backend session handlingderive_more: Error Formattingdiesel: ORM that Operates on Several Databasesdotenv: Configuration Loader (.env)envy: Deserializes Environment Variables into a Config Structjsonwebtoken: JWT encoding/decodingkcov: Coverage Analysislistenfd: Listens for Filesystem Changesrayon: Parallelizer2d2: Database Connection Poolingvalidator: Validates incoming Jsonrand: Generate random salt string
Clone the repo and cd into the repo:
git clone https://github.com/maitsarv/actix-simple-bp
cd actix-simple-bpCopy over the example .env file:
cp .env.example .envIMPORTANT: Change .env values for your setup, paying special attention to the salt and various keys and ports.
Next, you'll need to install the Diesel CLI:
cargo install diesel_cliIf you run into errors, see http://diesel.rs/guides/getting-started/
Now run the migrations via the Diesel CLI:
diesel migration runTo startup the server:
cargo runTo startup the server and autoreload on code changes:
systemfd --no-pid -s http::3000 -- cargo watch -x runIntegration tests are in the /src/tests folder. There are helper functions
to make testing the API straightforward. For example, if we want to test the
GET /api/v1/user route:
use crate::tests::helpers::tests::assert_get;
#[test]
async fn test_get_users() {
assert_get("/api/v1/user").await;
}Using the Actix test server, the request is sent and the response is asserted for a successful response:
assert!(response.status().is_success());
Similarly, to test a POST route:
use crate::handlers::user::CreateUserRequest;
use crate::tests::helpers::tests::assert_post;
#[test]
async fn test_create_user() {
let params = CreateUserRequest {
first_name: "Satoshi".into(),
last_name: "Nakamoto".into(),
email: "satoshi@nakamotoinstitute.org".into(),
};
assert_post("/api/v1/user", params).await;
}To run all of the tests:
cargo testTo build a Docker image of the application:
docker build -t rust_actix_example .Once the image is built, you can run the container in port 3000:
docker run -it --rm --env-file=.env.docker -p 3000:3000 --name rust_actix_example rust_actix_exampleStatic files are served up from the /static folder.
Directory listing is turned off.
Index files are supported (index.html).
Example:
curl -X GET http://127.0.0.1:3000/test.htmlTo serve static files to authenticated users only, place them in the /static-secure folder.
These files are referenced using the root-level /secure path.
Example:
curl -X GET http://127.0.0.1:3000/secure/test.htmlA shared, mutable hashmap is automatically added to the server. To invoke this data in a handler, simply add data: AppState<'_, String> to the function signature.
Retrieves a copy of the entry in application state by key.
Example:
use create::state::get;
pub async fn handle(data: AppState<'_, String>) -> impl Responder {
let key = "SOME_KEY";
let value = get(data, key);
assert_eq!(value, Some("123".to_string()));
}Inserts or updates an entry in application state.
Example:
use create::state::set;
pub async fn handle(data: AppState<'_, String>) -> impl Responder {
let key = "SOME_KEY";
let value = set(data, key, "123".into());
assert_eq!(value, None)); // if this is an insert
assert_eq!(value, Some("123".to_string())); // if this is an update
}Deletes an entry in application state by key.
Example:
use create::state::get;
pub async fn handle(data: AppState<'_, String>) -> impl Responder {
let key = "SOME_KEY";
let value = delete(data, key);
assert_eq!(value, None);
}Asynchronous access to redis is automatically added to the server if a value is provided for the REDIS_URL environment variable.
To invoke this data in a handler, simply add cache: Cache to the function signature.
Retrieves a copy of the entry in the application cache by key.
Example:
use crate::server_helpers:::cache::{get, Cache};
pub async fn handle(cache: Cache) -> impl Responder {
let key = "SOME_KEY";
let value = get(cache, key).await?;
assert_eq!(value, "123");
}Inserts or updates an entry in the application cache.
Example:
use crate::server_helpers:::cache::{set, Cache};
pub async fn handle(cache: Cache) -> impl Responder {
let key = "SOME_KEY";
set(cache, key, "123").await?;
}Deletes an entry in the application cache by key.
Example:
use crate::server_helpers:::cache::{delete, Cache};
pub async fn handle(cache: Cache) -> impl Responder {
let key = "SOME_KEY";
delete(cache, key).await?;
}When accessing a database via Diesel, operations block the main server thread. This blocking can be mitigated by running the blocking code in a thread pool from within the handler.
Example:
pub async fn get_user(
user_id: Path<Uuid>,
pool: Data<PoolType>,
) -> Result<Json<UserResponse>, ApiError> {
let user = block(move || find(&pool, *user_id)).await?;
respond_json(user)
}Blocking errors are automatically converted into ApiErrors to keep the api simple:
impl From<BlockingError<ApiError>> for ApiError {
fn from(error: BlockingError<ApiError>) -> ApiError {
match error {
BlockingError::Error(api_error) => api_error,
BlockingError::Canceled => ApiError::BlockingError("Thread blocking error".into()),
}
}
}Determine if the system is healthy.
GET /health
{
"status": "ok",
"version": "0.1.0"
}Example:
curl -X GET http://127.0.0.1:3000/healthPOST /api/v1/auth/login
| Param | Type | Description | Required | Validations |
|---|---|---|---|---|
| String | The user's email address | yes | valid email address | |
| password | String | The user's password | yes | at least 6 characters |
{
"email": "torvalds@transmeta.com",
"password": "123456"
}Header
HTTP/1.1 200 OK
content-length: 118
content-type: application/json
set-cookie: auth=COOKIE_VALUE_HERE; HttpOnly; Path=/; Max-Age=1200
date: Tue, 15 Oct 2019 02:04:54 GMTJson Body
{
"id": "0c419802-d1ef-47d6-b8fa-c886a23d61a7",
"first_name": "Linus",
"last_name": "Torvalds",
"email": "torvalds@transmeta.com"
}When sending subsequent requests, create a header variable cookie with the value auth=COOKIE_VALUE_HERE
GET /api/v1/auth/logout
200 OK
Example:
curl -X GET http://127.0.0.1:3000/api/v1/auth/logoutGET /api/v1/user
[
{
"id": "a421a56e-8652-4da6-90ee-59dfebb9d1b4",
"first_name": "Satoshi",
"last_name": "Nakamoto",
"email": "satoshi@nakamotoinstitute.org"
},
{
"id": "c63d285b-7794-4419-bfb7-86d7bb3ff17d",
"first_name": "Barbara",
"last_name": "Liskov",
"email": "bliskov@substitution.org"
}
]Example:
curl -X GET http://127.0.0.1:3000/api/v1/userGET /api/v1/user/{id}
| Param | Type | Description |
|---|---|---|
| id | Uuid | The user's id |
{
"id": "a421a56e-8652-4da6-90ee-59dfebb9d1b4",
"first_name": "Satoshi",
"last_name": "Nakamoto",
"email": "satoshi@nakamotoinstitute.org"
}Example:
curl -X GET http://127.0.0.1:3000/api/v1/user/a421a56e-8652-4da6-90ee-59dfebb9d1b4404 Not Found
{
"errors": ["User c63d285b-7794-4419-bfb7-86d7bb3ff17a not found"]
}POST /api/v1/user
| Param | Type | Description | Required | Validations |
|---|---|---|---|---|
| first_name | String | The user's first name | yes | at least 3 characters |
| last_name | String | The user's last name | yes | at least 3 characters |
| String | The user's email address | yes | valid email address |
{
"first_name": "Linus",
"last_name": "Torvalds",
"email": "torvalds@transmeta.com"
}{
"id": "0c419802-d1ef-47d6-b8fa-c886a23d61a7",
"first_name": "Linus",
"last_name": "Torvalds",
"email": "torvalds@transmeta.com"
}Example:
curl -X POST \
http://127.0.0.1:3000/api/v1/user \
-H 'Content-Type: application/json' \
-d '{
"first_name": "Linus",
"last_name": "Torvalds",
"email": "torvalds@transmeta.com"
}'422 Unprocessable Entity
{
"errors": [
"first_name is required and must be at least 3 characters",
"last_name is required and must be at least 3 characters",
"email must be a valid email"
]
}PUT /api/v1/{id}
Path
| Param | Type | Description |
|---|---|---|
| id | Uuid | The user's id |
Body
| Param | Type | Description | Required | Validations |
|---|---|---|---|---|
| first_name | String | The user's first name | yes | at least 3 characters |
| last_name | String | The user's last name | yes | at least 3 characters |
| String | The user's email address | yes | valid email address |
{
"first_name": "Linus",
"last_name": "Torvalds",
"email": "torvalds@transmeta.com"
}{
"id": "0c419802-d1ef-47d6-b8fa-c886a23d61a7",
"first_name": "Linus",
"last_name": "Torvalds",
"email": "torvalds@transmeta.com"
}Example:
curl -X PUT \
http://127.0.0.1:3000/api/v1/user/0c419802-d1ef-47d6-b8fa-c886a23d61a7 \
-H 'Content-Type: application/json' \
-d '{
"first_name": "Linus",
"last_name": "Torvalds",
"email": "torvalds@transmeta.com"
}'422 Unprocessable Entity
{
"errors": [
"first_name is required and must be at least 3 characters",
"last_name is required and must be at least 3 characters",
"email must be a valid email"
]
}404 Not Found
{
"errors": ["User 0c419802-d1ef-47d6-b8fa-c886a23d61a7 not found"]
}DELETE /api/v1/user/{id}
| Param | Type | Description |
|---|---|---|
| id | Uuid | The user's id |
{
"id": "a421a56e-8652-4da6-90ee-59dfebb9d1b4",
"first_name": "Satoshi",
"last_name": "Nakamoto",
"email": "satoshi@nakamotoinstitute.org"
}200 OK
Example:
curl -X DELETE http://127.0.0.1:3000/api/v1/user/a421a56e-8652-4da6-90ee-59dfebb9d1b4404 Not Found
{
"errors": ["User c63d285b-7794-4419-bfb7-86d7bb3ff17a not found"]
}This project is licensed under:
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)