diff --git a/examples/poem/tera-i18n/Cargo.toml b/examples/poem/tera-i18n/Cargo.toml
new file mode 100644
index 0000000000..8372564792
--- /dev/null
+++ b/examples/poem/tera-i18n/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "example-tera-i18n"
+version.workspace = true
+edition.workspace = true
+publish.workspace = true
+
+[dependencies]
+poem = { workspace = true, features = ["tera", "i18n"] }
+tokio = { workspace = true, features = ["full"] }
+once_cell = "1.17.0"
diff --git a/examples/poem/tera-i18n/resources/en-US/simple.ftl b/examples/poem/tera-i18n/resources/en-US/simple.ftl
new file mode 100644
index 0000000000..f1b4bf0077
--- /dev/null
+++ b/examples/poem/tera-i18n/resources/en-US/simple.ftl
@@ -0,0 +1,2 @@
+hello-world = Hello world!
+welcome = welcome { $name }!
diff --git a/examples/poem/tera-i18n/resources/fr/simple.ftl b/examples/poem/tera-i18n/resources/fr/simple.ftl
new file mode 100644
index 0000000000..e300799877
--- /dev/null
+++ b/examples/poem/tera-i18n/resources/fr/simple.ftl
@@ -0,0 +1,2 @@
+hello-world = Bonjour le monde!
+welcome = Bienvenue { $name }!
diff --git a/examples/poem/tera-i18n/resources/zh-CN/simple.ftl b/examples/poem/tera-i18n/resources/zh-CN/simple.ftl
new file mode 100644
index 0000000000..a9074f2d61
--- /dev/null
+++ b/examples/poem/tera-i18n/resources/zh-CN/simple.ftl
@@ -0,0 +1,2 @@
+hello-world = 你好世界!
+welcome = 欢迎 { $name }!
diff --git a/examples/poem/tera-i18n/src/main.rs b/examples/poem/tera-i18n/src/main.rs
new file mode 100644
index 0000000000..c6bced680c
--- /dev/null
+++ b/examples/poem/tera-i18n/src/main.rs
@@ -0,0 +1,37 @@
+use poem::{
+    ctx, get, handler,
+    i18n::I18NResources,
+    listener::TcpListener,
+    tera::{filters, Tera, TeraTemplate, TeraTemplating},
+    web::Path,
+    EndpointExt, Route, Server,
+};
+
+#[handler]
+fn index(tera: Tera) -> TeraTemplate {
+    tera.render("index.html.tera", &ctx! {})
+}
+
+#[handler]
+fn hello(Path(name): Path<String>, tera: Tera) -> TeraTemplate {
+    tera.render("hello.html.tera", &ctx! { "name": &name })
+}
+
+#[tokio::main]
+async fn main() -> Result<(), std::io::Error> {
+    let resources = I18NResources::builder()
+        .add_path("resources")
+        .build()
+        .unwrap();
+
+    let app = Route::new()
+        .at("/", get(index))
+        .at("/hello/:name", get(hello))
+        .with(TeraTemplating::from_glob("templates/**/*"))
+        .using(filters::i18n::translate)
+        .data(resources);
+
+    Server::new(TcpListener::bind("127.0.0.1:3000"))
+        .run(app)
+        .await
+}
diff --git a/examples/poem/tera-i18n/templates/hello.html.tera b/examples/poem/tera-i18n/templates/hello.html.tera
new file mode 100644
index 0000000000..5f15c30608
--- /dev/null
+++ b/examples/poem/tera-i18n/templates/hello.html.tera
@@ -0,0 +1 @@
+<h1>{{ "welcome" | translate(name=name) }}</h1>
\ No newline at end of file
diff --git a/examples/poem/tera-i18n/templates/index.html.tera b/examples/poem/tera-i18n/templates/index.html.tera
new file mode 100644
index 0000000000..27a75593ea
--- /dev/null
+++ b/examples/poem/tera-i18n/templates/index.html.tera
@@ -0,0 +1 @@
+<h1>{{ "hello-world" | translate }}</h1>
\ No newline at end of file
diff --git a/examples/poem/tera-templating/Cargo.toml b/examples/poem/tera-templating/Cargo.toml
index 934aeeb3b1..fc992645ff 100644
--- a/examples/poem/tera-templating/Cargo.toml
+++ b/examples/poem/tera-templating/Cargo.toml
@@ -5,7 +5,7 @@ edition.workspace = true
 publish.workspace = true
 
 [dependencies]
-poem.workspace = true
+poem = { workspace = true, features = ["tera"] }
 tokio = { workspace = true, features = ["full"] }
-tera = "1.17.1"
 once_cell = "1.17.0"
+tracing-subscriber.workspace = true
\ No newline at end of file
diff --git a/examples/poem/tera-templating/src/main.rs b/examples/poem/tera-templating/src/main.rs
index b5ce0e3ac6..e188cf5da4 100644
--- a/examples/poem/tera-templating/src/main.rs
+++ b/examples/poem/tera-templating/src/main.rs
@@ -1,38 +1,28 @@
-use once_cell::sync::Lazy;
 use poem::{
-    error::InternalServerError,
-    get, handler,
+    ctx, get, handler,
     listener::TcpListener,
-    web::{Html, Path},
-    Route, Server,
+    tera::{Tera, TeraTemplate, TeraTemplating},
+    web::Path,
+    EndpointExt, Route, Server,
 };
-use tera::{Context, Tera};
-
-static TEMPLATES: Lazy<Tera> = Lazy::new(|| {
-    let mut tera = match Tera::new("templates/**/*") {
-        Ok(t) => t,
-        Err(e) => {
-            println!("Parsing error(s): {e}");
-            ::std::process::exit(1);
-        }
-    };
-    tera.autoescape_on(vec![".html", ".sql"]);
-    tera
-});
 
 #[handler]
-fn hello(Path(name): Path<String>) -> Result<Html<String>, poem::Error> {
-    let mut context = Context::new();
-    context.insert("name", &name);
-    TEMPLATES
-        .render("index.html.tera", &context)
-        .map_err(InternalServerError)
-        .map(Html)
+fn hello(Path(name): Path<String>, tera: Tera) -> TeraTemplate {
+    tera.render("index.html.tera", &ctx! { "name": &name })
 }
 
 #[tokio::main]
 async fn main() -> Result<(), std::io::Error> {
-    let app = Route::new().at("/hello/:name", get(hello));
+    if std::env::var_os("RUST_LOG").is_none() {
+        std::env::set_var("RUST_LOG", "poem=debug");
+    }
+    tracing_subscriber::fmt::init();
+
+    let app = Route::new()
+        .at("/hello/:name", get(hello))
+        .with(TeraTemplating::from_glob("templates/**/*"))
+        .with_live_reloading();
+
     Server::new(TcpListener::bind("127.0.0.1:3000"))
         .run(app)
         .await
diff --git a/poem-openapi/src/base.rs b/poem-openapi/src/base.rs
index dd9a735f39..068b05ae5d 100644
--- a/poem-openapi/src/base.rs
+++ b/poem-openapi/src/base.rs
@@ -1,6 +1,7 @@
 use std::{
     collections::HashMap,
     fmt::{self, Debug, Display},
+    marker::Send,
     ops::Deref,
 };
 
@@ -198,7 +199,7 @@ pub trait ApiExtractor<'a>: Sized {
 }
 
 #[poem::async_trait]
-impl<'a, T: FromRequest<'a>> ApiExtractor<'a> for T {
+impl<'a, T: FromRequest<'a> + Send> ApiExtractor<'a> for T {
     const TYPE: ApiExtractorType = ApiExtractorType::PoemExtractor;
 
     type ParamType = ();
diff --git a/poem/Cargo.toml b/poem/Cargo.toml
index c9970230a2..1429eaabed 100644
--- a/poem/Cargo.toml
+++ b/poem/Cargo.toml
@@ -67,6 +67,9 @@ acme-base = [
 embed = ["rust-embed", "hex", "mime_guess"]
 xml = ["quick-xml"]
 yaml = ["serde_yaml"]
+templates = []
+live_reloading = ["dep:notify"]
+tera = ["dep:tera", "templates"]
 
 [dependencies]
 poem-derive.workspace = true
@@ -161,6 +164,8 @@ hex = { version = "0.4", optional = true }
 quick-xml = { workspace = true, optional = true }
 serde_yaml = { workspace = true, optional = true }
 tokio-stream = { workspace = true, optional = true }
+tera = { version = "1.17.1", optional = true }
+notify = { version = "5.1", optional = true }
 
 # Feature optional dependencies
 anyhow = { version = "1.0.0", optional = true }
diff --git a/poem/README.md b/poem/README.md
index e4d35ef65b..7978d66d03 100644
--- a/poem/README.md
+++ b/poem/README.md
@@ -51,7 +51,7 @@ which are disabled by default:
 
 | Feature       | Description                                                                               |
 |---------------|-------------------------------------------------------------------------------------------|
-| server        | Server and listener APIs (enabled by default)                                               |                                                     |
+| server        | Server and listener APIs (enabled by default)                                             |
 | compression   | Support decompress request body and compress response body                                |
 | cookie        | Support for Cookie                                                                        |
 | csrf          | Support for Cross-Site Request Forgery (CSRF) protection                                  |
@@ -76,7 +76,8 @@ which are disabled by default:
 | tokio-metrics | Integrate with [`tokio-metrics`](https://crates.io/crates/tokio-metrics) crate.           |
 | embed         | Integrate with [`rust-embed`](https://crates.io/crates/rust-embed) crate.                 |
 | xml           | Integrate with [`quick-xml`](https://crates.io/crates/quick-xml) crate.                   |
-| yaml           | Integrate with [`serde-yaml`](https://crates.io/crates/serde-yaml) crate.                   |
+| yaml          | Integrate with [`serde-yaml`](https://crates.io/crates/serde-yaml) crate.                 |
+| tera          | Support for [`tera`](https://crates.io/crates/tera) templating.                           |
 
 ## Safety
 
diff --git a/poem/src/i18n/locale.rs b/poem/src/i18n/locale.rs
index 584c26d77a..ffda1c40f6 100644
--- a/poem/src/i18n/locale.rs
+++ b/poem/src/i18n/locale.rs
@@ -86,7 +86,7 @@ impl Locale {
 
 #[async_trait::async_trait]
 impl<'a> FromRequest<'a> for Locale {
-    async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
+    fn from_request_sync(req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
         let resources = req
             .extensions()
             .get::<I18NResources>()
diff --git a/poem/src/lib.rs b/poem/src/lib.rs
index 80103cef90..e715097ac8 100644
--- a/poem/src/lib.rs
+++ b/poem/src/lib.rs
@@ -255,6 +255,7 @@
 //! | embed  | Integrate with [`rust-embed`](https://crates.io/crates/rust-embed) crate. |
 //! | xml | Integrate with [`quick-xml`](https://crates.io/crates/quick-xml) crate. |
 //! | yaml | Integrate with [`serde-yaml`](https://crates.io/crates/serde-yaml) crate.                   |
+//! | tera | Support for [`tera`](https://crates.io/crates/tera) templating. |
 
 #![doc(html_favicon_url = "https://raw.githubusercontent.com/poem-web/poem/master/favicon.ico")]
 #![doc(html_logo_url = "https://raw.githubusercontent.com/poem-web/poem/master/logo.png")]
@@ -276,6 +277,9 @@ pub mod middleware;
 #[cfg(feature = "session")]
 #[cfg_attr(docsrs, doc(cfg(feature = "session")))]
 pub mod session;
+#[cfg(feature = "templates")]
+#[cfg_attr(docsrs, doc(cfg(feature = "templates")))]
+pub mod templates;
 #[cfg(feature = "test")]
 #[cfg_attr(docsrs, doc(cfg(feature = "test")))]
 pub mod test;
diff --git a/poem/src/templates/live_reloading.rs b/poem/src/templates/live_reloading.rs
new file mode 100644
index 0000000000..4291cb6ac5
--- /dev/null
+++ b/poem/src/templates/live_reloading.rs
@@ -0,0 +1,77 @@
+use notify::{
+    Watcher as WatcherTrait,
+    Event, EventKind,
+    RecursiveMode
+};
+
+use std::sync::{
+    Arc, atomic::{ AtomicBool, Ordering },
+};
+
+pub(crate) struct Watcher {
+    pub(crate) needs_reload: Arc<AtomicBool>,
+    pub(crate) _path: String,
+    _watcher: Option<Arc<dyn WatcherTrait + Send + Sync + 'static>>
+}
+
+impl Watcher {
+    pub(crate) fn new(path: String) -> Self {
+        let needs_reload = Arc::new(AtomicBool::new(false));
+        let needs_reload_cloned = needs_reload.clone();
+
+        let watcher = notify::recommended_watcher(move |event| match event {
+            Ok(Event {
+                kind:
+                    EventKind::Create(_)
+                    | EventKind::Modify(_)
+                    | EventKind::Remove(_),
+                ..
+            }) => {
+                needs_reload.store(true, Ordering::Relaxed);
+                tracing::debug!("Sent reload request");
+            },
+            Err(e) => {
+                // Ignore errors for now and just output them.
+                // todo: make panic?
+                tracing::debug!("Watcher error: {e:?}");
+            },
+            _ => {},
+        });
+
+        let watcher = watcher
+            .map(|mut w| w
+                .watch(std::path::Path::new(&path), RecursiveMode::Recursive)
+                .map(|_| w)
+            );
+
+        let watcher = match watcher {
+            Ok(Ok(w)) => {
+                tracing::info!("Watching templates directory `{path}` for changes.");
+
+                Some(Arc::new(w) as Arc<dyn WatcherTrait + Send + Sync>)
+            }
+            Err(e) | Ok(Err(e)) => {
+                tracing::error!("Failed to start watcher: {e}");
+                tracing::debug!("Watcher error: {e:?}");
+
+                None
+            },
+        };
+
+        Self {
+            needs_reload: needs_reload_cloned,
+            _path: path,
+            _watcher: watcher,
+        }
+    }
+
+    pub(crate) fn needs_reload(&self) -> bool {
+        self.needs_reload.swap(false, Ordering::Relaxed)
+    }
+}
+
+pub enum LiveReloading {
+    Enabled(String),
+    Debug(String),
+    Disabled
+}
\ No newline at end of file
diff --git a/poem/src/templates/mod.rs b/poem/src/templates/mod.rs
new file mode 100644
index 0000000000..287a7a9537
--- /dev/null
+++ b/poem/src/templates/mod.rs
@@ -0,0 +1,14 @@
+mod template; pub use template::Template;
+
+
+
+#[cfg(feature = "templates")]
+#[cfg_attr(docsrs, doc(cfg(feature = "templates")))]
+pub mod tera;
+
+#[cfg(feature = "live_reloading")]
+#[cfg_attr(docsrs, doc(cfg(feature = "live_reloading")))]
+mod live_reloading;
+
+#[cfg(feature = "live_reloading")]
+pub use live_reloading::LiveReloading;
\ No newline at end of file
diff --git a/poem/src/templates/template.rs b/poem/src/templates/template.rs
new file mode 100644
index 0000000000..078ba969bb
--- /dev/null
+++ b/poem/src/templates/template.rs
@@ -0,0 +1,56 @@
+use crate::{
+    IntoResponse, Response,
+    http::StatusCode,
+};
+
+/// An engine-agnostic template to be rendered.
+///
+/// This response type requires a templating engine middleware
+/// to work correctly. Missing the middleware will return
+/// `500 Internal Server Error`.
+///
+/// ```
+/// use poem::{
+///     templates::Template,
+///     ctx, handler,
+///     web::Path,
+/// };
+///
+/// #[handler]
+/// fn hello(Path(name): Path<String>) -> Template {
+///     Template::render("index.html.tera", &ctx! { "name": &name })
+/// }
+/// ```
+pub struct Template<C> {
+    /// Path to the template.
+    pub name: String,
+    /// Template context. This is used
+    /// by engines for additional data.
+    pub context: C,
+}
+
+impl<C> Template<C> {
+    /// Renders the template.
+    pub fn render(name: impl Into<String>, context: C) -> Self {
+        Self {
+            name: name.into(),
+            context,
+        }
+    }
+}
+
+impl<C: Send + Sync + 'static> IntoResponse for Template<C> {
+    fn into_response(self) -> Response {
+        // At this stage, we respond with an internal server error,
+        // as we have not yet built the template.
+        Response::builder()
+            .status(StatusCode::INTERNAL_SERVER_ERROR)
+
+            // We add this as an extension so that it can be
+            // accessed by the endpoint to actually render
+            // the template.
+            .extension(self)
+
+            .finish()
+    }
+}
\ No newline at end of file
diff --git a/poem/src/templates/tera/middleware.rs b/poem/src/templates/tera/middleware.rs
new file mode 100644
index 0000000000..c346666b86
--- /dev/null
+++ b/poem/src/templates/tera/middleware.rs
@@ -0,0 +1,219 @@
+use tera::Tera;
+
+use super::Flavor;
+
+use crate::{
+    templates::Template,
+    error::{InternalServerError, IntoResult},
+    web::Html,
+    Endpoint, Middleware, Request, Result,
+    Response, IntoResponse,
+};
+
+#[cfg(feature = "live_reloading")]
+use crate::templates::live_reloading::{ Watcher, LiveReloading };
+
+/// Tera template with context.
+pub type TeraTemplate = Template<tera::Context>;
+
+/// Tera templates middleware.
+pub struct TeraEngine {
+    tera: Tera,
+}
+
+impl TeraEngine {
+    /// Create a new instance of `TeraEngine`, containing all the parsed
+    /// templates found in the glob The errors are already handled.
+    ///
+    /// ```no_run
+    /// use poem::templates::tera::TeraEngine;
+    ///
+    /// let tera = TeraEngine::from_glob("templates/**/*")
+    ///     .expect("Failed to load templates");
+    /// ```
+    pub fn from_glob(glob: &str) -> tera::Result<Self> {
+        Ok(Self {
+            tera: Tera::new(glob)?
+        })
+    }
+
+    /// Create a new instance of `TeraEngine`, containing all the parsed
+    /// templates found in the directory.
+    ///
+    /// ```no_run
+    /// use poem::templates::tera::TeraEngine;
+    ///
+    /// let tera = TeraEngine::from_directory("templates")
+    ///     .expect("Failed to load templates");
+    /// ```
+    pub fn from_directory(template_directory: &str) -> tera::Result<Self> {
+        Self::from_glob(&format!("{template_directory}/**/*"))
+    }
+
+    /// Create a new instance of `TeraEngine`, using a provided `Tera`
+    /// instance.
+    ///
+    /// ```no_run
+    /// use poem::templates::tera::{TeraEngine, Tera};
+    ///
+    /// let mut tera = Tera::new("templates/**/*").expect("Failed to parse templates");
+    ///
+    /// tera.autoescape_on(vec![".html", ".sql"]);
+    /// let engine = TeraEngine::custom(tera);
+    /// ```
+    pub fn custom(tera: Tera) -> Self {
+        Self { tera }
+    }
+}
+
+impl Default for TeraEngine {
+    fn default() -> Self {
+        Self::from_directory("templates")
+            .expect("Failed to load templates")
+    }
+}
+
+impl<E: Endpoint> Middleware<E> for TeraEngine {
+    type Output = TeraEndpoint<E>;
+
+    fn transform(&self, inner: E) -> Self::Output {
+        Self::Output {
+            tera: Flavor::Immutable(self.tera.clone()),
+            inner,
+            transformers: Vec::new(),
+        }
+    }
+}
+
+/// Tera templates endpoint.
+pub struct TeraEndpoint<E> {
+    tera: Flavor,
+    inner: E,
+    transformers: Vec<Box<dyn Fn(&mut Tera, &mut Request) + Send + Sync + 'static>>,
+}
+
+#[async_trait::async_trait]
+impl<E: Endpoint> Endpoint for TeraEndpoint<E> {
+    type Output = Response;
+
+    async fn call(&self, mut req: Request) -> Result<Self::Output> {
+        let mut tera = match &self.tera {
+            Flavor::Immutable(t) => t.clone(),
+
+            #[cfg(feature = "live_reloading")]
+            Flavor::LiveReload { tera, watcher } => {
+                let lock = if watcher.needs_reload() {
+                    tracing::info!("Detected changes to templates, reloading...");
+
+                    let mut lock = tera.write().await;
+
+                    if let Err(e) = lock.full_reload() {
+                        tracing::error!("Failed to reload templates: {e}");
+                        tracing::debug!("Reload templates error: {e:?}");
+
+                        return Err(InternalServerError(e));
+                    }
+
+                    lock.downgrade()
+                } else {
+                    tera.read().await
+                };
+
+                lock.clone()
+            }
+        };
+
+        for transformer in &self.transformers {
+            transformer(&mut tera, &mut req);
+        }
+
+        let response = self.inner.call(req).await?.into_response();
+
+        match response.extensions().get::<TeraTemplate>() {
+            Some(template) => {
+                let result = tera.render(&template.name, &template.context);
+
+                if let Err(e) = &result {
+                    tracing::debug!("Tera Rendering error: {e:?}");
+                    tracing::error!("Failed to render Tera template: {e}");
+                }
+
+                result.map(|s| Html(s).into_response())
+                    .map_err(InternalServerError)
+            },
+            None => {
+                // todo: this destroys the type
+                response.into_result()
+            }
+        }
+    }
+}
+
+impl<E: Endpoint> TeraEndpoint<E> {
+    /// Add a transformer that apply changes to each tera instances (for
+    /// instance, registering a dynamic filter) before passing tera to
+    /// request handlers
+    ///
+    /// ```no_run
+    /// use poem::{Route, EndpointExt, templates::tera::TeraEngine};
+    ///
+    /// let app = Route::new()
+    ///     .with(TeraEngine::default())
+    ///     .using(|tera, req| println!("{tera:?}\n{req:?}"));
+    /// ```
+    pub fn using<F>(mut self, transformer: F) -> Self where
+        F: Fn(&mut Tera, &mut Request) + Send + Sync + 'static
+    {
+        self.transformers.push(Box::new(transformer));
+        self
+    }
+
+    /// Toggle live reloading. Defaults to enabled for debug and
+    /// disabled for release builds.
+    ///
+    /// ```no_run
+    /// use poem::{Route, EndpointExt, templates::tera::TeraEngine};
+    ///
+    /// let app = Route::new()
+    ///     .with(TeraEngine::default())
+    ///     .with_live_reloading(LiveReloading::Disabled);
+    /// ```
+
+    #[cfg(feature = "live_reloading")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "live_reloading")))]
+    pub fn with_live_reloading(mut self, live_reloading: LiveReloading) -> Self {
+        self.tera = match (self.tera, live_reloading) {
+            #[cfg(debug_assertions)]
+            (Flavor::Immutable(tera), LiveReloading::Debug(path)) => {
+                tracing::debug!("Live reloading for Tera templates is enabled");
+
+                Flavor::LiveReload { tera: tokio::sync::RwLock::new(tera), watcher: Watcher::new(path) }
+            },
+
+            (Flavor::Immutable(tera), LiveReloading::Enabled(path)) => {
+                tracing::debug!("Live reloading for Tera templates is enabled");
+
+                Flavor::LiveReload { tera: tokio::sync::RwLock::new(tera), watcher: Watcher::new(path) }
+            },
+
+            #[cfg(not(debug_assertions))]
+            (Flavor::LiveReload { tera, .. }, LiveReloading::Debug(_)) => {
+                tracing::debug!("Live reloading for Tera templates is disabled");
+
+                Flavor::Immutable(tera.into_inner())
+            },
+
+            (Flavor::LiveReload { tera, .. }, LiveReloading::Disabled) => {
+                tracing::debug!("Live reloading for Tera templates is disabled");
+
+                Flavor::Immutable(tera.into_inner())
+            },
+
+            // todo: enable changing watch path
+
+            (tera, _) => tera
+        };
+
+        self
+    }
+}
diff --git a/poem/src/templates/tera/mod.rs b/poem/src/templates/tera/mod.rs
new file mode 100644
index 0000000000..d851a94c8f
--- /dev/null
+++ b/poem/src/templates/tera/mod.rs
@@ -0,0 +1,74 @@
+//! Tera Templating Support
+//!
+//! # Load templates from file system using a glob
+//!
+//! ```no_run
+//! use poem::templates::tera::TeraEngine;
+//!
+//! let tera = TeraEngine::default();
+//! ```
+//!
+//! # Render a template inside an handler with some context vars
+//!
+//! ```
+//! use poem::{
+//!     ctx, handler,
+//!     templates::Template,
+//!     web::Path,
+//! };
+//!
+//! #[handler]
+//! fn hello(Path(name): Path<String>) -> Template<_> {
+//!     Template::render("index.html.tera", &ctx! { "name": &name })
+//! }
+//! ```
+
+mod middleware;
+mod transformers;
+
+pub use tera::{ Context, Tera };
+
+pub use self::{
+    middleware::{ TeraEndpoint, TeraEngine, TeraTemplate },
+    transformers::filters,
+};
+
+enum Flavor {
+    Immutable(Tera),
+
+    #[cfg(feature = "live_reloading")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "live_reloading")))]
+    LiveReload {
+        tera: tokio::sync::RwLock<Tera>,
+        watcher: super::live_reloading::Watcher
+    }
+}
+
+/// Macro for constructing a Tera Context
+/// ```
+/// use poem::{
+///     ctx, handler,
+///     templates::Template,
+///     web::Path,
+/// };
+///
+/// #[handler]
+/// fn hello(Path(name): Path<String>) -> Template<_> {
+///     Template::render("index.html.tera", &ctx! { "name": &name })
+/// }
+/// ```
+
+// todo: create common macro with common context
+
+#[macro_export]
+macro_rules! ctx {
+    { $( $key:literal: $value:expr ),* } => {
+        {
+            let mut context = ::poem::tera::Context::new();
+            $(
+                context.insert($key, $value);
+            )*
+            context
+        }
+    };
+}
diff --git a/poem/src/templates/tera/transformers.rs b/poem/src/templates/tera/transformers.rs
new file mode 100644
index 0000000000..71197e90dd
--- /dev/null
+++ b/poem/src/templates/tera/transformers.rs
@@ -0,0 +1,88 @@
+/// Tera templates built-in filters
+pub mod filters {
+    /// Tera templates built-in i18n filters
+    #[cfg(feature = "i18n")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "i18n")))]
+    pub mod i18n {
+        use std::{borrow::Cow, collections::HashMap};
+
+        use fluent::{
+            types::{FluentNumber, FluentNumberOptions},
+            FluentValue,
+        };
+        use tera::{self, Filter, Tera, Value};
+
+        use crate::{i18n::Locale, FromRequest, Request};
+
+        /// Tera templates i18n filter
+        ///
+        /// ```no_run
+        /// use poem::{Route, EndpointExt, i18n::I18NResources, templates::tera::{TeraEngine, filters}};
+        ///
+        /// let resources = I18NResources::builder()
+        ///     .add_path("resources")
+        ///     .build()
+        ///     .unwrap();
+        ///
+        /// let app = Route::new()
+        ///     .with(TeraEngine::default())
+        ///     .using(filters::i18n::translate)
+        ///     .data(resources);
+        /// ```
+        pub struct TranslateFilter {
+            locale: Locale,
+        }
+
+        impl Filter for TranslateFilter {
+            fn filter(&self, id: &Value, args: &HashMap<String, Value>) -> tera::Result<Value> {
+                if args.len() == 0 {
+                    self.locale.text(id.as_str().unwrap())
+                } else {
+                    let mut fluent_args = HashMap::new();
+                    for (key, value) in args {
+                        fluent_args.insert(
+                            key.as_str(),
+                            match value {
+                                Value::Null => FluentValue::None,
+                                Value::Number(val) => FluentValue::Number(FluentNumber::new(
+                                    val.as_f64().unwrap(),
+                                    FluentNumberOptions::default(),
+                                )),
+                                Value::String(val) => FluentValue::String(Cow::Owned(val.clone())),
+                                _ => FluentValue::Error,
+                            },
+                        );
+                    }
+                    self.locale
+                        .text_with_args(id.as_str().unwrap(), fluent_args)
+                }
+                .map(|str| Value::String(str))
+                .map_err(|err| tera::Error::msg(err))
+            }
+        }
+
+        /// Tera Templating built-in filters
+        ///
+        /// ```no_run
+        /// use poem::{Route, EndpointExt, i18n::I18NResources, templates::tera::{TeraEngine, filters}};
+        ///
+        /// let resources = I18NResources::builder()
+        ///     .add_path("resources")
+        ///     .build()
+        ///     .unwrap();
+        ///
+        /// let app = Route::new()
+        ///     .with(TeraEngine::default())
+        ///     .using(filters::i18n::translate)
+        ///     .data(resources);
+        /// ```
+        pub fn translate(tera: &mut Tera, req: &mut Request) {
+            tera.register_filter(
+                "translate",
+                TranslateFilter {
+                    locale: Locale::from_request_without_body_sync(req).unwrap(),
+                },
+            );
+        }
+    }
+}
diff --git a/poem/src/web/mod.rs b/poem/src/web/mod.rs
index 144e7bd634..cafda38aae 100644
--- a/poem/src/web/mod.rs
+++ b/poem/src/web/mod.rs
@@ -312,7 +312,9 @@ impl RequestBody {
 #[async_trait::async_trait]
 pub trait FromRequest<'a>: Sized {
     /// Extract from request head and body.
-    async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result<Self>;
+    async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result<Self> {
+        Self::from_request_sync(req, body)
+    }
 
     /// Extract from request head.
     ///
@@ -326,6 +328,16 @@ pub trait FromRequest<'a>: Sized {
     async fn from_request_without_body(req: &'a Request) -> Result<Self> {
         Self::from_request(req, &mut Default::default()).await
     }
+
+    /// Extract from request head and body synchronously.
+    fn from_request_sync(_req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
+        panic!("Not implemented, please implement one of from_request and from_request_sync");
+    }
+
+    /// Extract from request head synchronously.
+    fn from_request_without_body_sync(req: &'a Request) -> Result<Self> {
+        Self::from_request_sync(req, &mut Default::default())
+    }
 }
 
 /// Represents a type that can convert into response.
@@ -807,14 +819,14 @@ impl<'a> FromRequest<'a> for &'a LocalAddr {
 }
 
 #[async_trait::async_trait]
-impl<'a, T: FromRequest<'a>> FromRequest<'a> for Option<T> {
+impl<'a, T: FromRequest<'a> + Send> FromRequest<'a> for Option<T> {
     async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result<Self> {
         Ok(T::from_request(req, body).await.ok())
     }
 }
 
 #[async_trait::async_trait]
-impl<'a, T: FromRequest<'a>> FromRequest<'a> for Result<T> {
+impl<'a, T: FromRequest<'a> + Send> FromRequest<'a> for Result<T> {
     async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result<Self> {
         Ok(T::from_request(req, body).await)
     }