-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make routing setup for SPAs easier #87
Comments
I am hoping to cut https://github.com/thedodd/trunk over to axum, but I have a use case where users may define arbitrary proxies for their applications, and the current routing paradigm does not seem to support the possibility of matching any request "below" a specific prefix. EG, if a user defines a proxy endpoint at |
@programatik29 looks like that might be perfect! Thanks for the quick response. |
A possible routing setup for SPAs could be: use axum::{prelude::*, routing::nest, service::ServiceExt};
use http::StatusCode;
use std::{convert::Infallible, net::SocketAddr};
use tower_http::services::{ServeDir, ServeFile};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = nest(
"/",
axum::service::get(ServeFile::new("assets/index.html")),
)
.nest(
"/assets",
axum::service::get(ServeDir::new("assets")),
)
.handle_error(|err| {
Ok::<_, Infallible>((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled error: {}", err),
))
})
.route("/ws", axum::ws::ws(handle_socket));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::debug!("listening on {}", addr);
hyper::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn handle_socket(socket: axum::ws::WebSocket) {
// ...
} main difference being that assets are served at |
Another possible setup: use std::net::SocketAddr;
use axum::routing::nest;
use axum::routing::RoutingDsl;
use tower::{service_fn, BoxError, Service};
use tower_http::services::{ServeDir, ServeFile};
use http_body::combinators::BoxBody;
#[tokio::main]
async fn main() {
let app = nest(
"/",
axum::service::get(service_fn(|req| async {
let resp = ServeDir::new("html").call(req).await?;
let resp = resp.map(|body| BoxBody::new(body));
if resp.status() == 404 {
let resp = ServeFile::new("html/index.html").call(()).await?;
let resp = resp.map(|body| BoxBody::new(body));
return Ok(resp);
}
Ok::<_, BoxError>(resp)
})),
);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
hyper::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
} Creative usage of |
Another approach, using use axum::{handler::get, http::StatusCode, response::IntoResponse, service, Router};
use std::{convert::Infallible, io, net::SocketAddr};
use tower_http::services::{ServeDir, ServeFile};
#[tokio::main]
async fn main() {
// our API routes
let api_routes = Router::new().route("/users", get(|| async { "users#index" }));
let app = Router::new()
// serve static files at `GET /assets/*`
.nest(
"/assets",
service::get(ServeDir::new("assets")).handle_error(handle_io_error),
)
// serve the API at `/api/*`
.nest("/api", api_routes)
// all other requests will receive `index.html` regardless of HTTP method or URI
.or(service::any(ServeFile::new("index.html")).handle_error(handle_io_error));
// run
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
fn handle_io_error(error: io::Error) -> Result<impl IntoResponse, Infallible> {
Ok((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled error: {}", error),
))
} |
I think I'll close this for now. I think the examples shared here are good starting points. If someone has specific questions feel free to ask here. |
I am trying to somewhat combine the last two approaches. I want to serve my SPA files at the root and have API routes. I tried serving my SPA files in Basically I want the following: Router::new()
// Serve API
.nest("/api", get_api_router())
// Serve SPA
.nest(
"/",
get_service(
ServeDir::new("./static")
)
.handle_error(|_: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "Internal Server Error"})),
)
}),
)
// Unknown routes to index.html for client side routing
.fallback(
get_service(
ServeFile::new("./static/index.html")
)
.handle_error(|_: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "Internal Server Error"})),
)
}),
); This does not work because In actix-web I was able to do this: App::new()
.service(get_api_scope())
.service(Files::new("/", "./static").index_file("index.html"))
.default_service(|r: ServiceRequest| futures::future::ok(spa_index(r)));
fn spa_index(service_request: ServiceRequest) -> ServiceResponse {
let (req, _payload) = service_request.into_parts();
let file_response = NamedFile::open("./static/index.html").respond_to(&req);
ServiceResponse::new(req, file_response.map_into_boxed_body())
} Can someone point me in the right direction? Thanks! |
@davidpdrsn I think that link is broken |
For SPAs its often useful to first check if the URI matches some static resource (like javascript or css) and if not fallback to calling a handler. The handler should receive the entire URI, regardless of what it is, so routing can be done client side.
Its might be possible to do such a setup with axum today using some combination of
ServeDir
(from tower-http) andnest
but I wouldn't be very ergonomic. We should investigate ways to make it easier.I'm not exactly sure what the best and most general solution is. Could be wildcard routes similarly to what tide has. I think looking into how tide handles setting up something like this and then learning from that is a good place to start.
The text was updated successfully, but these errors were encountered: