diff --git a/Cargo.lock b/Cargo.lock
index 2af949be..b347dafa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -32,6 +32,17 @@ version = "1.0.51"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
 
+[[package]]
+name = "async-trait"
+version = "0.1.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -114,6 +125,7 @@ name = "cargo-binstall"
 version = "0.5.0"
 dependencies = [
  "anyhow",
+ "async-trait",
  "cargo_metadata",
  "cargo_toml",
  "crates-index",
diff --git a/Cargo.toml b/Cargo.toml
index 8e3cafd4..5eccc5d3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,6 +41,7 @@ crates-index = "0.18.1"
 semver = "1.0.4"
 xz2 = "0.1.6"
 zip = "0.5.13"
+async-trait = "0.1.52"
 
 [dev-dependencies]
 env_logger = "0.9.0"
diff --git a/src/bins.rs b/src/bins.rs
new file mode 100644
index 00000000..26c33865
--- /dev/null
+++ b/src/bins.rs
@@ -0,0 +1,106 @@
+use std::path::PathBuf;
+
+use cargo_toml::Product;
+use serde::Serialize;
+
+use crate::{Template, PkgFmt, PkgMeta};
+
+pub struct BinFile {
+    pub base_name: String,
+    pub source: PathBuf,
+    pub dest: PathBuf,
+    pub link: PathBuf,
+}
+
+impl BinFile {
+    pub fn from_product(data: &Data, product: &Product) -> Result<Self, anyhow::Error> {
+        let base_name = product.name.clone().unwrap();
+
+        // Generate binary path via interpolation
+        let ctx = Context { 
+            name: &data.name,
+            repo: data.repo.as_ref().map(|s| &s[..]),
+            target: &data.target, 
+            version: &data.version,
+            format: if data.target.contains("windows") { ".exe" } else { "" },
+            bin: &base_name,
+        };
+
+        // Generate install paths
+        // Source path is the download dir + the generated binary path
+        let source_file_path = ctx.render(&data.meta.bin_dir)?;
+        let source = if data.meta.pkg_fmt == PkgFmt::Bin {
+            data.bin_path.clone()
+        } else {
+            data.bin_path.join(&source_file_path)
+        };
+
+        // Destination path is the install dir + base-name-version{.format}
+        let dest_file_path = ctx.render("{ bin }-v{ version }{ format }")?; 
+        let dest = data.install_path.join(dest_file_path);
+
+        // Link at install dir + base name
+        let link = data.install_path.join(&base_name);
+
+        Ok(Self { base_name, source, dest, link })
+    }
+
+    pub fn preview_bin(&self) -> String {
+        format!("{} ({} -> {})", self.base_name, self.source.file_name().unwrap().to_string_lossy(), self.dest.display())
+    }
+
+    pub fn preview_link(&self) -> String {
+        format!("{} ({} -> {})", self.base_name, self.dest.display(), self.link.display())
+    }
+
+    pub fn install_bin(&self) -> Result<(), anyhow::Error> {
+        // TODO: check if file already exists
+        std::fs::copy(&self.source, &self.dest)?;
+
+        #[cfg(target_family = "unix")]
+        {
+            use std::os::unix::fs::PermissionsExt;
+            std::fs::set_permissions(&self.dest, std::fs::Permissions::from_mode(0o755))?;
+        }
+
+        Ok(())
+    }
+
+    pub fn install_link(&self) -> Result<(), anyhow::Error> {
+        // Remove existing symlink
+        // TODO: check if existing symlink is correct
+        if self.link.exists() {
+            std::fs::remove_file(&self.link)?;
+        }
+
+        #[cfg(target_family = "unix")]
+        std::os::unix::fs::symlink(&self.dest, &self.link)?;
+        #[cfg(target_family = "windows")]
+        std::os::windows::fs::symlink_file(&self.dest, &self.link)?;
+
+        Ok(())
+    }
+}
+
+/// Data required to get bin paths
+pub struct Data {
+    pub name: String,
+    pub target: String,
+    pub version: String,
+    pub repo: Option<String>,
+    pub meta: PkgMeta,
+    pub bin_path: PathBuf,
+    pub install_path: PathBuf,
+}
+
+#[derive(Clone, Debug, Serialize)]
+struct Context<'c> {
+    pub name: &'c str,
+    pub repo: Option<&'c str>,
+    pub target: &'c str,
+    pub version: &'c str,
+    pub format: &'c str,
+    pub bin: &'c str,
+}
+
+impl<'c> Template for Context<'c> {}
\ No newline at end of file
diff --git a/src/fetchers.rs b/src/fetchers.rs
new file mode 100644
index 00000000..85a29a51
--- /dev/null
+++ b/src/fetchers.rs
@@ -0,0 +1,30 @@
+use std::path::Path;
+
+pub use gh_release::*;
+
+use crate::PkgMeta;
+
+mod gh_release;
+
+#[async_trait::async_trait]
+pub trait Fetcher {
+    /// Create a new fetcher from some data
+    async fn new(data: &Data) -> Result<Self, anyhow::Error>
+    where
+        Self: std::marker::Sized;
+
+    /// Fetch a package
+    async fn fetch(&self, dst: &Path) -> Result<(), anyhow::Error>;
+    
+    /// Check if a package is available for download
+    async fn check(&self) -> Result<bool, anyhow::Error>;
+}
+
+/// Data required to fetch a package
+pub struct Data {
+    pub name: String,
+    pub target: String,
+    pub version: String,
+    pub repo: Option<String>,
+    pub meta: PkgMeta,
+}
\ No newline at end of file
diff --git a/src/fetchers/gh_release.rs b/src/fetchers/gh_release.rs
new file mode 100644
index 00000000..17bf4a79
--- /dev/null
+++ b/src/fetchers/gh_release.rs
@@ -0,0 +1,50 @@
+use std::path::Path;
+
+use log::{debug, info};
+use serde::Serialize;
+
+use crate::{download, head, Template};
+use super::Data;
+
+pub struct GhRelease {
+    url: String,
+}
+
+#[async_trait::async_trait]
+impl super::Fetcher for GhRelease {
+    async fn new(data: &Data) -> Result<Self, anyhow::Error> {
+        // Generate context for URL interpolation
+        let ctx = Context { 
+            name: &data.name,
+            repo: data.repo.as_ref().map(|s| &s[..]),
+            target: &data.target, 
+            version: &data.version,
+            format: data.meta.pkg_fmt.to_string(),
+        };
+        debug!("Using context: {:?}", ctx);
+
+        Ok(Self { url: ctx.render(&data.meta.pkg_url)? })
+    }
+
+    async fn check(&self) -> Result<bool, anyhow::Error> {
+        info!("Checking for package at: '{}'", self.url);
+        head(&self.url).await
+    }
+
+    async fn fetch(&self, dst: &Path) -> Result<(), anyhow::Error> {
+        info!("Downloading package from: '{}'", self.url);
+        download(&self.url, dst).await
+    }
+}
+
+/// Template for constructing download paths
+#[derive(Clone, Debug, Serialize)]
+struct Context<'c> {
+    pub name: &'c str,
+    pub repo: Option<&'c str>,
+    pub target: &'c str,
+    pub version: &'c str,
+    pub format: String,
+}
+
+impl<'c> Template for Context<'c> {}
\ No newline at end of file
diff --git a/src/helpers.rs b/src/helpers.rs
index dce5c9f3..216fa9fe 100644
--- a/src/helpers.rs
+++ b/src/helpers.rs
@@ -5,7 +5,9 @@ use log::{debug, info, error};
 
 use cargo_toml::{Manifest};
 use flate2::read::GzDecoder;
+use serde::Serialize;
 use tar::Archive;
+use tinytemplate::TinyTemplate;
 use xz2::read::XzDecoder;
 use zip::read::ZipArchive;
 
@@ -24,6 +26,11 @@ pub fn load_manifest_path<P: AsRef<Path>>(manifest_path: P) -> Result<Manifest<M
     Ok(manifest)
 }
 
+pub async fn head(url: &str) -> Result<bool, anyhow::Error> {
+    let req = reqwest::Client::new().head(url).send().await?;
+    Ok(req.status().is_success())
+}
+
 /// Download a file from the provided URL to the provided path
 pub async fn download<P: AsRef<Path>>(url: &str, path: P) -> Result<(), anyhow::Error> {
 
@@ -149,3 +156,17 @@ pub fn confirm() -> Result<bool, anyhow::Error> {
         }
     }
 }
+
+pub trait Template: Serialize {
+    fn render(&self, template: &str) -> Result<String, anyhow::Error>
+    where Self: Sized {
+        // Create template instance
+        let mut tt = TinyTemplate::new();
+
+        // Add template to instance
+        tt.add_template("path", &template)?;
+
+        // Render output
+        Ok(tt.render("path", self)?)
+    }
+}
\ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
index b8564f38..bd668427 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,8 +3,6 @@ use std::collections::HashMap;
 
 use serde::{Serialize, Deserialize};
 use strum_macros::{Display, EnumString, EnumVariantNames};
-use tinytemplate::TinyTemplate;
-
 
 pub mod helpers;
 pub use helpers::*;
@@ -12,6 +10,9 @@ pub use helpers::*;
 pub mod drivers;
 pub use drivers::*;
 
+pub mod bins;
+pub mod fetchers;
+
 
 /// Compiled target triple, used as default for binary fetching
 pub const TARGET: &'static str = env!("TARGET");
@@ -141,33 +142,6 @@ pub struct BinMeta {
     pub path: String,
 }
 
-/// Template for constructing download paths
-#[derive(Clone, Debug, Serialize)]
-pub struct Context {
-    pub name: String,
-    pub repo: Option<String>,
-    pub target: String,
-    pub version: String,
-    pub format: String,
-    pub bin: Option<String>,
-}
-
-impl Context {
-    /// Render the context into the provided template
-    pub fn render(&self, template: &str) -> Result<String, anyhow::Error> {
-        // Create template instance
-        let mut tt = TinyTemplate::new();
-
-        // Add template to instance
-        tt.add_template("path", &template)?;
-
-        // Render output
-        let rendered = tt.render("path", self)?;
-
-        Ok(rendered)
-    }
-}
-
 #[cfg(test)]
 mod test {
     use crate::{load_manifest_path};
diff --git a/src/main.rs b/src/main.rs
index 6f7cf72e..7b809684 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,7 +7,7 @@ use structopt::StructOpt;
 
 use tempdir::TempDir;
 
-use cargo_binstall::*;
+use cargo_binstall::{*, fetchers::{GhRelease, Data, Fetcher}, bins};
 
 
 #[derive(Debug, StructOpt)]
@@ -109,37 +109,34 @@ async fn main() -> Result<(), anyhow::Error> {
 
     debug!("Found metadata: {:?}", meta);
 
-    // Generate context for URL interpolation
-    let ctx = Context { 
-        name: opts.name.clone(), 
-        repo: package.repository, 
-        target: opts.target.clone(), 
-        version: package.version.clone(),
-        format: meta.pkg_fmt.to_string(),
-        bin: None,
-    };
-
-    debug!("Using context: {:?}", ctx);
-    
-    // Interpolate version / target / etc.
-    let rendered = ctx.render(&meta.pkg_url)?;
-
     // Compute install directory
-    let install_path = match get_install_path(opts.install_path.as_deref()) {
-        Some(p) => p,
-        None => {
-            error!("No viable install path found of specified, try `--install-path`");
-            return Err(anyhow::anyhow!("No install path found or specified"));
-        }
-    };
-
+    let install_path = get_install_path(opts.install_path.as_deref()).ok_or_else(|| {
+        error!("No viable install path found of specified, try `--install-path`");
+        anyhow::anyhow!("No install path found or specified")
+    })?;
     debug!("Using install path: {}", install_path.display());
 
-    info!("Downloading package from: '{}'", rendered);
+    // Compute temporary directory for downloads
+    let pkg_path = temp_dir.path().join(format!("pkg-{}.{}", opts.name, meta.pkg_fmt));
+    debug!("Using temporary download path: {}", pkg_path.display());
+
+    let fetcher_data = Data {
+        name: package.name.clone(),
+        target: opts.target.clone(),
+        version: package.version.clone(),
+        repo: package.repository.clone(),
+        meta: meta.clone(),
+    };
+    
+    // Try github releases
+    let gh = GhRelease::new(&fetcher_data).await?;
+    if !gh.check().await? {
+        error!("No file found in github releases, cannot proceed");
+        return Err(anyhow::anyhow!("No viable remote package found"));
+    }
 
     // Download package
-    let pkg_path = temp_dir.path().join(format!("pkg-{}.{}", opts.name, meta.pkg_fmt));
-    download(&rendered, pkg_path.to_str().unwrap()).await?;
+    gh.fetch(&pkg_path).await?;
 
     #[cfg(incomplete)]
     {
@@ -182,49 +179,30 @@ async fn main() -> Result<(), anyhow::Error> {
 
     // List files to be installed
     // based on those found via Cargo.toml
-    let bin_files = binaries.iter().map(|p| {
-        // Fetch binary base name
-        let base_name = p.name.clone().unwrap();
+    let bin_data = bins::Data {
+        name: package.name.clone(),
+        target: opts.target.clone(),
+        version: package.version.clone(),
+        repo: package.repository.clone(),
+        meta,
+        bin_path,
+        install_path,
+    };
 
-        // Generate binary path via interpolation
-        let mut bin_ctx = ctx.clone();
-        bin_ctx.bin = Some(base_name.clone());
-        
-        // Append .exe to windows binaries
-        bin_ctx.format = match &opts.target.clone().contains("windows") {
-            true => ".exe".to_string(),
-            false => "".to_string(),
-        };
-
-        // Generate install paths
-        // Source path is the download dir + the generated binary path
-        let source_file_path = bin_ctx.render(&meta.bin_dir)?;
-        let source = if meta.pkg_fmt == PkgFmt::Bin {
-            bin_path.clone()
-        } else {
-            bin_path.join(&source_file_path)
-        };
-
-        // Destination path is the install dir + base-name-version{.format}
-        let dest_file_path = bin_ctx.render("{ bin }-v{ version }{ format }")?; 
-        let dest = install_path.join(dest_file_path);
-
-        // Link at install dir + base name
-        let link = install_path.join(&base_name);
-
-        Ok((base_name, source, dest, link))
-    }).collect::<Result<Vec<_>, anyhow::Error>>()?;
+    let bin_files = binaries.iter()
+        .map(|p| bins::BinFile::from_product(&bin_data, p))
+        .collect::<Result<Vec<_>, anyhow::Error>>()?;
 
     // Prompt user for confirmation
     info!("This will install the following binaries:");
-    for (name, source, dest, _link) in &bin_files {
-        info!("  - {} ({} -> {})", name, source.file_name().unwrap().to_string_lossy(), dest.display());
+    for file in &bin_files {
+        info!("  - {}", file.preview_bin());
     }
 
     if !opts.no_symlinks {
         info!("And create (or update) the following symlinks:");
-        for (name, _source, dest, link) in &bin_files {
-            info!("  - {} ({} -> {})", name, dest.display(), link.display());
+        for file in &bin_files {
+            info!("  - {}", file.preview_link());
         }
     }
 
@@ -234,37 +212,17 @@ async fn main() -> Result<(), anyhow::Error> {
     }
 
     info!("Installing binaries...");
-
-    // Install binaries
-    for (_name, source, dest, _link) in &bin_files {
-        // TODO: check if file already exists
-        std::fs::copy(source, dest)?;
-
-        #[cfg(target_family = "unix")]
-        {
-            use std::os::unix::fs::PermissionsExt;
-            std::fs::set_permissions(dest, std::fs::Permissions::from_mode(0o755))?;
-        }
+    for file in &bin_files {
+        file.install_bin()?;
     }
 
     // Generate symlinks
     if !opts.no_symlinks {
-        for (_name, _source, dest, link) in &bin_files {
-            // Remove existing symlink
-            // TODO: check if existing symlink is correct
-            if link.exists() {
-                std::fs::remove_file(&link)?;
-            }
-
-            #[cfg(target_family = "unix")]
-            std::os::unix::fs::symlink(dest, link)?;
-            #[cfg(target_family = "windows")]
-            std::os::windows::fs::symlink_file(dest, link)?;
+        for file in &bin_files {
+            file.install_link()?;
         }
     }
     
     info!("Installation complete!");
-
     Ok(())
-}
-
+}
\ No newline at end of file